diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 03338475..b5d7ca2e 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -68,38 +68,8 @@ namespace MobileGL::MG_Impl::GLImpl { // limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource. constexpr GLint kFrontendMaxComputeUniformComponents = static_cast(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(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE); - constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = - static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE); constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768; constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024; - constexpr GLint kFrontendMaxCombinedAtomicCounters = - static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE); - constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers = - static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE); - constexpr GLint kFrontendMaxFragmentAtomicCounters = - static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE); - constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers = - static_cast(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(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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 23218c93..2f9945e9 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -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__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_ 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(stages) & (1u << static_cast(kStages[stage]))) != 0; + }; + + const Int blockCount = reflection.getNumUniformBlocks(); + if (blockCount <= 0) return {}; + Vector isCounterBlock(static_cast(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(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(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 diff --git a/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp index bbc9f66c..dbb2d0e0 100644 --- a/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp @@ -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__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 diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index b954000f..26481f4f 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -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_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(params); + + const GLint fullBuffer = static_cast(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(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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp index dbb417a2..9afff0eb 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp @@ -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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 1b40996d..b4b6a952 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/Types.h b/MobileGL/MG_Util/ShaderTranspiler/Types.h index c9242414..051ce951 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/Types.h +++ b/MobileGL/MG_Util/ShaderTranspiler/Types.h @@ -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__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__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