[Fix] (GL): derive per-stage atomic-counter limits from the backend's storage-block budget and fail the link past them

This commit is contained in:
2026-10-12 08:32:47 -04:00
parent cc23ed3c7e
commit 35077242c3
7 changed files with 337 additions and 66 deletions
+26 -44
View File
@@ -68,38 +68,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
constexpr GLint kFrontendMaxComputeUniformComponents =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_COMPUTE_UNIFORM_COMPONENTS);
// Every atomic-counter limit is shared with the glslang resource table
// (BuildTBuiltInResource) through MG_Util/ShaderTranspiler/Types.h: GL 4.6 requires
// glGetIntegerv and the gl_MaxAtomicCounter* built-in constants to agree, and the two
// used to be independent tables that disagreed on both the binding count and the buffer
// size. Never move one of these without the other.
constexpr GLint kFrontendMaxComputeAtomicCounters =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
constexpr GLint kFrontendMaxCombinedAtomicCounters =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxFragmentAtomicCounters =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
// Zero counters means zero buffers to hold them. These have to be ANSWERED rather than
// left to the default INVALID_ENUM: a well-behaved application queries the limit exactly
// to find out that the stage cannot do this, and an error instead both leaves its output
// untouched (so it reads uninitialised memory and may conclude the opposite) and leaves a
// GL error pending that surfaces at whatever unrelated call checks next.
constexpr GLint kFrontendMaxGeometryAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
// at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that
// the getter, the indexed queries and glBindBufferBase all share one ceiling.
@@ -333,6 +303,18 @@ namespace MobileGL::MG_Impl::GLImpl {
// ARM's GLES driver reports 0 for all four (a Mali-G925 does), and advertising 16 there
// bought nothing: the program still failed to link inside the backend, the frontend
// still reported LINK_STATUS as true, and every draw with it silently rendered nothing.
// Every per-stage and combined atomic-counter limit. Shared with the glslang resource
// table (BuildTBuiltInResource) through ResolveAtomicCounterLimits: GL 4.6 requires
// glGetIntegerv and the gl_Max*AtomicCounter* built-in constants to agree, and the link
// fails a program over them. A stage that cannot host a counter answers 0 rather than
// INVALID_ENUM - an application queries the limit exactly to find that out.
MG_Util::ShaderTranspiler::AtomicCounterLimits AdvertisedAtomicCounterLimits() {
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
return MG_Util::ShaderTranspiler::ResolveAtomicCounterLimits(
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
: kBackendlessDefaults);
}
GLint StageStorageBlockCount(Int MG_Backend::DynamicBackendParameters::*stageLimit) {
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
const MG_Backend::DynamicBackendParameters& parameters =
@@ -1885,10 +1867,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxComputeUniformComponents;
return;
case GL_MAX_COMPUTE_ATOMIC_COUNTERS:
*params = kFrontendMaxComputeAtomicCounters;
*params = AdvertisedAtomicCounterLimits().stageCounters[5];
return;
case GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxComputeAtomicCounterBuffers;
*params = AdvertisedAtomicCounterLimits().stageBuffers[5];
return;
case GL_MAX_COMPUTE_SHARED_MEMORY_SIZE:
*params = kFrontendMaxComputeSharedMemorySize;
@@ -2081,10 +2063,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
return;
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
*params = kFrontendMaxCombinedAtomicCounters;
*params = AdvertisedAtomicCounterLimits().combinedCounters;
return;
case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxCombinedAtomicCounterBuffers;
*params = AdvertisedAtomicCounterLimits().combinedBuffers;
return;
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
@@ -2099,10 +2081,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 1024 * 1024; // TODO
return;
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
*params = kFrontendMaxFragmentAtomicCounters;
*params = AdvertisedAtomicCounterLimits().stageCounters[4];
return;
case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxFragmentAtomicCounterBuffers;
*params = AdvertisedAtomicCounterLimits().stageBuffers[4];
return;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
@@ -2125,10 +2107,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks);
return;
case GL_MAX_GEOMETRY_ATOMIC_COUNTERS:
*params = kFrontendMaxGeometryAtomicCounters;
*params = AdvertisedAtomicCounterLimits().stageCounters[3];
return;
case GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxGeometryAtomicCounterBuffers;
*params = AdvertisedAtomicCounterLimits().stageBuffers[3];
return;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
@@ -2191,16 +2173,16 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = INT_MAX; // TODO
return;
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS:
*params = kFrontendMaxTessControlAtomicCounters;
*params = AdvertisedAtomicCounterLimits().stageCounters[1];
return;
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxTessControlAtomicCounterBuffers;
*params = AdvertisedAtomicCounterLimits().stageBuffers[1];
return;
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS:
*params = kFrontendMaxTessEvaluationAtomicCounters;
*params = AdvertisedAtomicCounterLimits().stageCounters[2];
return;
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxTessEvaluationAtomicCounterBuffers;
*params = AdvertisedAtomicCounterLimits().stageBuffers[2];
return;
case GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS:
*params = MG_Backend::pActiveBackendObject
@@ -2286,10 +2268,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxVaryingVectors;
return;
case GL_MAX_VERTEX_ATOMIC_COUNTERS:
*params = kFrontendMaxVertexAtomicCounters;
*params = AdvertisedAtomicCounterLimits().stageCounters[0];
return;
case GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxVertexAtomicCounterBuffers;
*params = AdvertisedAtomicCounterLimits().stageBuffers[0];
return;
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*params = MG_Backend::pActiveBackendObject
@@ -363,9 +363,8 @@ namespace {
// 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.
// Atomic counter buffers are lowered storage blocks too, but GL counts them against their
// own limits: ValidateAtomicCounterLimits below.
static MobileGL::String ValidateBlockLimits(glslang::TProgram& reflection,
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
using MobileGL::Array;
@@ -451,6 +450,95 @@ namespace {
return {};
}
// GL 4.6 core 7.7: LinkProgram FAILS when a stage uses more atomic counter buffers or atomic
// counters than GL_MAX_<STAGE>_ATOMIC_COUNTER_BUFFERS / _ATOMIC_COUNTERS, or the stages
// together more than the GL_MAX_COMBINED_* pair (a buffer or counter two stages use counts in
// each). Nothing enforced either, so a geometry stage with more counters than advertised
// linked cleanly. The limits are ResolveAtomicCounterLimits', the derivation glGetIntegerv
// and the gl_Max*AtomicCounter* constants answer from, and they follow what the backend can
// host - a counter buffer is a lowered storage block in every stage - so this refuses nothing
// the backend could have run.
//
// A counter buffer is one synthesized gl_AtomicCounterBlock_<binding> block; a counter is one
// element of one of its members. Both are counted only in the stages that reference them.
static MobileGL::String ValidateAtomicCounterLimits(glslang::TProgram& reflection,
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
using MobileGL::Int;
using MobileGL::SizeT;
using MobileGL::Vector;
namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler;
constexpr SizeT kStageCount = Transpiler::ATOMIC_COUNTER_STAGE_COUNT;
static constexpr EShLanguage kStages[kStageCount] = {EShLangVertex, EShLangTessControl, EShLangTessEvaluation,
EShLangGeometry, EShLangFragment, EShLangCompute};
static constexpr const char* kStageNames[kStageCount] = {"VERTEX", "TESS_CONTROL", "TESS_EVALUATION",
"GEOMETRY", "FRAGMENT", "COMPUTE"};
const auto inStage = [](EShLanguageMask stages, SizeT stage) {
return (static_cast<unsigned>(stages) & (1u << static_cast<unsigned>(kStages[stage]))) != 0;
};
const Int blockCount = reflection.getNumUniformBlocks();
if (blockCount <= 0) return {};
Vector<bool> isCounterBlock(static_cast<SizeT>(blockCount), false);
Int buffers[kStageCount] = {};
bool anyCounterBlock = false;
for (Int i = 0; i < blockCount; ++i) {
const auto& block = reflection.getUniformBlock(i);
if (!IsAtomicCounterBlockName(block.name)) continue;
isCounterBlock[static_cast<SizeT>(i)] = true;
anyCounterBlock = true;
for (SizeT stage = 0; stage < kStageCount; ++stage) {
if (inStage(block.stages, stage)) ++buffers[stage];
}
}
if (!anyCounterBlock) return {}; // every program that declares no atomic counter
Int counters[kStageCount] = {};
const Int uniformCount = reflection.getNumUniformVariables();
for (Int i = 0; i < uniformCount; ++i) {
const auto& uniform = reflection.getUniform(i);
if (uniform.index < 0 || uniform.index >= blockCount || !isCounterBlock[static_cast<SizeT>(uniform.index)]) {
continue;
}
Int elements = uniform.size > 1 ? uniform.size : 1;
if (const glslang::TType* type = uniform.getType(); type != nullptr && type->isSizedArray()) {
elements = type->getCumulativeArraySize();
}
for (SizeT stage = 0; stage < kStageCount; ++stage) {
if (inStage(uniform.stages, stage)) counters[stage] += elements;
}
}
const Transpiler::AtomicCounterLimits limits = Transpiler::ResolveAtomicCounterLimits(env.params);
Int combinedBuffers = 0;
Int combinedCounters = 0;
for (SizeT stage = 0; stage < kStageCount; ++stage) {
if (buffers[stage] > limits.stageBuffers[stage]) {
return std::format("This program uses {} atomic counter buffers in one stage, more than the {} "
"GL_MAX_{}_ATOMIC_COUNTER_BUFFERS allows.",
buffers[stage], limits.stageBuffers[stage], kStageNames[stage]);
}
if (counters[stage] > limits.stageCounters[stage]) {
return std::format("This program uses {} atomic counters in one stage, more than the {} "
"GL_MAX_{}_ATOMIC_COUNTERS allows.",
counters[stage], limits.stageCounters[stage], kStageNames[stage]);
}
combinedBuffers += buffers[stage];
combinedCounters += counters[stage];
}
if (combinedBuffers > limits.combinedBuffers) {
return std::format("This program uses {} atomic counter buffers across its stages, more than the {} "
"GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS allows.",
combinedBuffers, limits.combinedBuffers);
}
if (combinedCounters > limits.combinedCounters) {
return std::format("This program uses {} atomic counters across its stages, more than the {} "
"GL_MAX_COMBINED_ATOMIC_COUNTERS allows.",
combinedCounters, limits.combinedCounters);
}
return {};
}
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
const auto* type = output.getType();
return type && type->getQualifier().builtIn != glslang::EbvNone;
@@ -1251,6 +1339,14 @@ namespace MobileGL::MG_State::GLState {
return false;
}
if (String counterLimitError = ValidateAtomicCounterLimits(*artifacts.program, env);
!counterLimitError.empty()) {
artifacts.infoLog = Move(counterLimitError);
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
@@ -924,6 +924,56 @@ void main() { color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrem
ClearErrors();
}
// GL 4.6 core 7.7 fails the link when a stage uses more atomic counter buffers than
// GL_MAX_<STAGE>_ATOMIC_COUNTER_BUFFERS. Nothing counted them, so a geometry stage with more
// counter buffers than advertised linked cleanly. Sized off the ADVERTISED limits, which
// follow the active backend's geometry-stage storage-block budget.
TEST_F(ProgramInterfaceTest, AtomicCounterBuffersOverAStageLimitFailToLink) {
GLint maxGeometryBuffers = -1;
GLint maxBindings = -1;
GetIntegerv(GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS, &maxGeometryBuffers);
GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &maxBindings);
ClearErrors();
ASSERT_GE(maxGeometryBuffers, 0);
// One counter per binding 0..buffers-1, each its own buffer.
const auto link = [](GLint buffers, std::string& log) {
std::string gs = R"(#version 430
layout(points) in;
layout(points, max_vertices = 1) out;
)";
std::string body;
for (GLint i = 0; i < buffers; ++i) {
gs += "layout(binding = " + std::to_string(i) + ") uniform atomic_uint c" + std::to_string(i) + ";\n";
body += " atomicCounterIncrement(c" + std::to_string(i) + ");\n";
}
gs += "void main() {\n" + body + " gl_Position = vec4(0.0);\n EmitVertex();\n}\n";
const char* gsSource = gs.c_str();
const GLuint p = MakeProgram(kSimpleVs, kSimpleFs);
const GLuint sh = CreateShader(GL_GEOMETRY_SHADER);
ShaderSource(sh, 1, &gsSource, nullptr);
CompileShader(sh);
AttachShader(p, sh);
LinkProgram(p);
GLint status = -1;
GetProgramiv(p, GL_LINK_STATUS, &status);
char buffer[4096] = "";
GetProgramInfoLog(p, sizeof(buffer), nullptr, buffer);
log = buffer;
return status == GL_TRUE;
};
std::string log;
if (maxGeometryBuffers + 1 <= maxBindings) {
EXPECT_FALSE(link(maxGeometryBuffers + 1, log)) << "one buffer over the advertised limit must not link";
EXPECT_NE(log.find("GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS"), std::string::npos) << "info log was: " << log;
}
if (maxGeometryBuffers > 0) {
EXPECT_TRUE(link(maxGeometryBuffers, log)) << "exactly the advertised limit must link: " << log;
}
ClearErrors();
}
// GL 4.6 core 7.6 fails the link when a stage's active image uniforms exceed
// GL_MAX_*_IMAGE_UNIFORMS, or when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing
// counted them - glslang keeps those numbers only so gl_Max*ImageUniforms can expand from
+78
View File
@@ -1230,6 +1230,84 @@ void main() {
MG_State::pGLContext = Move(previousContext);
}
// Every per-stage atomic-counter limit follows the backend's per-stage STORAGE-BLOCK budget,
// because both backends serve a counter buffer as a lowered storage block in any stage: a stage
// with storage blocks gets counter buffers (up to the binding count) and a full buffer's worth of
// counters, a stage without gets 0 of both. glGetIntegerv and the gl_Max*AtomicCounter*
// constants must say the same thing (KHR-GL43.shader_atomic_counters.basic-glsl-built-in).
TEST(GetterSanity, AtomicCounterLimitsFollowTheStageStorageBlockBudget) {
using namespace MobileGL;
namespace Transpiler = MG_Util::ShaderTranspiler;
auto previousContext = MG_State::pGLContext.Take();
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::DynamicBackendParameters params{};
params.MaxShaderStorageBufferBindings = 24;
params.MaxCombinedShaderStorageBlocks = 24;
params.MaxVertexShaderStorageBlocks = 4;
params.MaxTessControlShaderStorageBlocks = 0;
params.MaxTessEvaluationShaderStorageBlocks = 0;
params.MaxGeometryShaderStorageBlocks = 2;
params.MaxFragmentShaderStorageBlocks = 4;
params.MaxComputeShaderStorageBlocks = 24;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
const GLint fullBuffer = static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE / 4);
const struct {
GLenum buffersPname;
GLenum countersPname;
GLint buffers;
GLint counters;
const char* buffersConstant;
const char* countersConstant;
} expected[] = {
{GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS, GL_MAX_VERTEX_ATOMIC_COUNTERS, 4, fullBuffer,
"gl_MaxVertexAtomicCounterBuffers", "gl_MaxVertexAtomicCounters"},
{GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS, GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS, 0, 0,
"gl_MaxTessControlAtomicCounterBuffers", "gl_MaxTessControlAtomicCounters"},
{GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS, GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS, 0, 0,
"gl_MaxTessEvaluationAtomicCounterBuffers", "gl_MaxTessEvaluationAtomicCounters"},
{GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS, GL_MAX_GEOMETRY_ATOMIC_COUNTERS, 2, fullBuffer,
"gl_MaxGeometryAtomicCounterBuffers", "gl_MaxGeometryAtomicCounters"},
{GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS, GL_MAX_FRAGMENT_ATOMIC_COUNTERS, 4, fullBuffer,
"gl_MaxFragmentAtomicCounterBuffers", "gl_MaxFragmentAtomicCounters"},
{GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS, GL_MAX_COMPUTE_ATOMIC_COUNTERS,
static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS), fullBuffer,
"gl_MaxComputeAtomicCounterBuffers", "gl_MaxComputeAtomicCounters"},
// The combined pair: the stage sum (4 + 2 + 4 + 8 buffers), bounded by the combined
// storage-block budget the lowered blocks draw from.
{GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS, GL_MAX_COMBINED_ATOMIC_COUNTERS, 18, 4 * fullBuffer,
"gl_MaxCombinedAtomicCounterBuffers", "gl_MaxCombinedAtomicCounters"},
};
String checks;
for (const auto& limit : expected) {
GLint reported = -1;
MG_Impl::GLImpl::GetIntegerv(limit.buffersPname, &reported);
EXPECT_EQ(reported, limit.buffers) << limit.buffersConstant;
reported = -1;
MG_Impl::GLImpl::GetIntegerv(limit.countersPname, &reported);
EXPECT_EQ(reported, limit.counters) << limit.countersConstant;
// A negative array size is a compile error, so each line compiles only while the
// constant expands to exactly the number glGetIntegerv answers.
checks += "int mg_" + String(limit.buffersConstant) + "[" + limit.buffersConstant +
" == " + std::to_string(limit.buffers) + " ? 1 : -1];\n";
checks += "int mg_" + String(limit.countersConstant) + "[" + limit.countersConstant +
" == " + std::to_string(limit.counters) + " ? 1 : -1];\n";
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const String source = "#version 430 core\nout vec4 color;\n" + checks + "void main() { color = vec4(1.0); }\n";
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_FRAGMENT_SHADER,
.sourceStr = source,
});
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
// KHR-GL43.compute_shader.max: the test queries every GL_MAX_COMPUTE_* value through the API and
// then makes a compute shader compare the matching gl_MaxCompute* constant against it. The two
// used to be independent tables and gl_MaxComputeWorkGroupSize.z disagreed - glslang compiled
@@ -83,6 +83,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// gl_MaxCullDistances / gl_MaxCombinedClipAndCullDistances from the same numbers.
HashValue(state, env.params.MaxCullDistances);
HashValue(state, env.params.MaxCombinedClipAndCullDistances);
// The inputs of ResolveAtomicCounterLimits, which every gl_Max*AtomicCounter* constant
// expands from: the per-stage counter-buffer budget is the stage's storage-block budget.
HashValue(state, env.params.MaxVertexShaderStorageBlocks);
HashValue(state, env.params.MaxTessControlShaderStorageBlocks);
HashValue(state, env.params.MaxTessEvaluationShaderStorageBlocks);
HashValue(state, env.params.MaxGeometryShaderStorageBlocks);
HashValue(state, env.params.MaxFragmentShaderStorageBlocks);
HashValue(state, env.params.MaxComputeShaderStorageBlocks);
HashValue(state, env.params.MaxCombinedShaderStorageBlocks);
HashValue(state, env.params.MaxShaderStorageBufferBindings);
// The texture-image-unit family, made env-derived in the same wave. They were stock
// glslang defaults (32/32/80) that disagreed with what glGetIntegerv answered, and
// gl_MaxTextureImageUnits / gl_MaxVertexTextureImageUnits / gl_MaxCombinedTextureImageUnits
@@ -118,8 +118,6 @@ namespace MobileGL {
Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS;
Resources.maxComputeTextureImageUnits = 16;
Resources.maxComputeImageUniforms = 8;
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxVaryingComponents = MAX_VARYING_COMPONENTS;
Resources.maxVertexOutputComponents = 64;
Resources.maxGeometryInputComponents = 64;
@@ -153,25 +151,14 @@ namespace MobileGL {
Resources.maxPatchVertices = 32;
Resources.maxTessGenLevel = 64;
Resources.maxViewports = 16;
Resources.maxVertexAtomicCounters = 0;
Resources.maxTessControlAtomicCounters = 0;
Resources.maxTessEvaluationAtomicCounters = 0;
Resources.maxGeometryAtomicCounters = 0;
Resources.maxFragmentAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxCombinedAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
// Every atomic-counter limit below is the one glGetIntegerv answers; the shared
// constants in Types.h are what keeps the two sides from drifting apart again.
// gl_MaxAtomicCounterBindings and gl_MaxAtomicCounterBufferSize expand from these
// (Initialize.cpp), and the binding count is also the ceiling glslang checks a
// `layout(binding = N) uniform atomic_uint` against - it was 1, so every counter
// outside binding 0 failed to compile.
// The per-stage and combined counter counts are env-derived and set further down.
Resources.maxAtomicCounterBindings = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS;
Resources.maxVertexAtomicCounterBuffers = 0;
Resources.maxTessControlAtomicCounterBuffers = 0;
Resources.maxTessEvaluationAtomicCounterBuffers = 0;
Resources.maxGeometryAtomicCounterBuffers = 0;
Resources.maxFragmentAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxCombinedAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
Resources.maxTransformFeedbackBuffers = 4;
Resources.maxTransformFeedbackInterleavedComponents = 64;
@@ -253,6 +240,27 @@ namespace MobileGL {
// declaration with a diagnostic the application can read.
Resources.maxCullDistances = dynamicParameters.MaxCullDistances;
Resources.maxCombinedClipAndCullDistances = dynamicParameters.MaxCombinedClipAndCullDistances;
// The per-stage and combined atomic-counter limits, from the same derivation
// glGetIntegerv answers with (ResolveAtomicCounterLimits): a stage's counter
// buffers are lowered storage blocks, so they follow the backend's per-stage
// storage-block budget, and gl_Max*AtomicCounter* has to expand to that number.
{
const AtomicCounterLimits counterLimits = ResolveAtomicCounterLimits(dynamicParameters);
Resources.maxVertexAtomicCounters = counterLimits.stageCounters[0];
Resources.maxTessControlAtomicCounters = counterLimits.stageCounters[1];
Resources.maxTessEvaluationAtomicCounters = counterLimits.stageCounters[2];
Resources.maxGeometryAtomicCounters = counterLimits.stageCounters[3];
Resources.maxFragmentAtomicCounters = counterLimits.stageCounters[4];
Resources.maxComputeAtomicCounters = counterLimits.stageCounters[5];
Resources.maxCombinedAtomicCounters = counterLimits.combinedCounters;
Resources.maxVertexAtomicCounterBuffers = counterLimits.stageBuffers[0];
Resources.maxTessControlAtomicCounterBuffers = counterLimits.stageBuffers[1];
Resources.maxTessEvaluationAtomicCounterBuffers = counterLimits.stageBuffers[2];
Resources.maxGeometryAtomicCounterBuffers = counterLimits.stageBuffers[3];
Resources.maxFragmentAtomicCounterBuffers = counterLimits.stageBuffers[4];
Resources.maxComputeAtomicCounterBuffers = counterLimits.stageBuffers[5];
Resources.maxCombinedAtomicCounterBuffers = counterLimits.combinedBuffers;
}
// The compute work-group limits are the env's, not the backend parameters': they
// are the only ones that come from a REAL indexed driver query, which
+52 -5
View File
@@ -74,11 +74,58 @@ namespace MobileGL {
// advertised binding point and binds them all in one glBindBuffersRange, so it must
// stay a multiple of, and comfortably larger than, four times the binding count.
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_SIZE = 16384;
// GL_MAX_{FRAGMENT,COMPUTE,COMBINED}_ATOMIC_COUNTER_BUFFERS and the matching
// _ATOMIC_COUNTERS. Eight is the GL 4.6 core minimum for the compute stage
// (table 23.45) and every other stage this implementation serves counters on.
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8;
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8;
// The ceilings of GL_MAX_<STAGE>_ATOMIC_COUNTER_BUFFERS and _ATOMIC_COUNTERS for a
// stage that can hold a counter buffer at all; ResolveAtomicCounterLimits below turns
// them into the advertised numbers. A stage can never use more counter buffers than
// there are counter bindings, and a counter is one uint of a lowered storage block, so
// the only bound on the counter count is how many fit in one buffer.
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS;
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = MAX_ATOMIC_COUNTER_BUFFER_SIZE / 4;
// Index order of AtomicCounterLimits' per-stage arrays.
inline constexpr SizeT ATOMIC_COUNTER_STAGE_COUNT = 6; // VS, TCS, TES, GS, FS, CS
struct AtomicCounterLimits {
Int stageBuffers[ATOMIC_COUNTER_STAGE_COUNT] = {};
Int stageCounters[ATOMIC_COUNTER_STAGE_COUNT] = {};
Int combinedBuffers = 0;
Int combinedCounters = 0;
};
// THE one derivation of every per-stage and combined atomic-counter limit, read by
// glGetIntegerv, by BuildTBuiltInResource (the gl_Max*AtomicCounter* constants) and by
// the link-time check, so the three cannot disagree. Both backends serve a counter in
// ANY stage the same way - glslang lowers it onto a storage block - so a stage's
// counter-buffer budget is its storage-block budget (as GL_MAX_<STAGE>_SHADER_STORAGE_
// BLOCKS clamps it), and a stage with none has no counters either. That is also why
// the pre-fragment stages are no longer a flat 0: they get exactly what the backend
// can host, and 0 only where it really cannot (a driver without vertex-stage storage
// blocks still answers 0, and the conformance suite skips the stage).
inline AtomicCounterLimits ResolveAtomicCounterLimits(const MG_Backend::DynamicBackendParameters& p) {
const Int storageBindings = std::max(p.MaxShaderStorageBufferBindings, 0);
const auto clampStorage = [storageBindings](Int limit) {
return std::min(std::max(limit, 0), storageBindings);
};
const Int stageStorageBlocks[ATOMIC_COUNTER_STAGE_COUNT] = {
p.MaxVertexShaderStorageBlocks, p.MaxTessControlShaderStorageBlocks,
p.MaxTessEvaluationShaderStorageBlocks, p.MaxGeometryShaderStorageBlocks,
p.MaxFragmentShaderStorageBlocks, p.MaxComputeShaderStorageBlocks};
AtomicCounterLimits limits;
Int bufferSum = 0;
for (SizeT stage = 0; stage < ATOMIC_COUNTER_STAGE_COUNT; ++stage) {
const Int buffers =
std::min(MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE, clampStorage(stageStorageBlocks[stage]));
limits.stageBuffers[stage] = buffers;
limits.stageCounters[stage] = buffers > 0 ? MAX_ATOMIC_COUNTERS_PER_STAGE : 0;
bufferSum += buffers;
limits.combinedCounters += limits.stageCounters[stage];
}
// A buffer two stages use counts once per stage (GL 4.6 7.7), so the combined
// ceiling is the stage sum, bounded by the combined storage-block budget the
// lowered blocks really draw from.
limits.combinedBuffers = std::min(bufferSum, clampStorage(p.MaxCombinedShaderStorageBlocks));
return limits;
}
// 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