diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 001656df..41326404 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -134,6 +134,81 @@ namespace { return {}; } + // GL 4.6 core 7.6: LinkProgram FAILS when a stage's count of active image uniforms exceeds + // GL_MAX_{VERTEX,TESS_CONTROL,TESS_EVALUATION,GEOMETRY,FRAGMENT,COMPUTE}_IMAGE_UNIFORMS, or + // when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing enforced it: glslang carries + // those numbers in TBuiltInResource only so gl_Max*ImageUniforms can expand from them, and + // its linker never counts uniforms against them - so a program declaring one image uniform + // more than the limit linked cleanly and then rendered nothing. + // + // The limits are the ones glGetIntegerv answers (MG_Impl/GLImpl/Getter/GL_Getter.cpp), the + // hardcoded tessellation zeros included: a program may not exceed a limit the implementation + // advertises, whatever the driver underneath would have taken. + // + // Counts the APPLICATION's image uniforms. The DirectGLES read/write split emits a second + // declaration for an image a stage both reads and writes (MG_Backend/DirectGLES/Utils.h), but + // that happens in the backend after this link, and counting the expanded set here would + // reject programs that are legal by the numbers GL advertises. Returns the info-log line for + // a program over a limit, empty for one within them. + static MobileGL::String ValidateImageUniformLimits( + glslang::TProgram& reflection, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { + using MobileGL::Int; + using MobileGL::SizeT; + + static constexpr EShLanguage kStages[] = {EShLangVertex, EShLangTessControl, EShLangTessEvaluation, + EShLangGeometry, EShLangFragment, EShLangCompute}; + static constexpr const char* kLimitNames[] = { + "GL_MAX_VERTEX_IMAGE_UNIFORMS", "GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS", + "GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS", "GL_MAX_GEOMETRY_IMAGE_UNIFORMS", + "GL_MAX_FRAGMENT_IMAGE_UNIFORMS", "GL_MAX_COMPUTE_IMAGE_UNIFORMS"}; + constexpr SizeT kStageCount = sizeof(kStages) / sizeof(kStages[0]); + const Int limits[kStageCount] = {env.params.MaxVertexImageUniforms, + 0, + 0, + env.params.MaxGeometryImageUniforms, + env.params.MaxFragmentImageUniforms, + env.params.MaxComputeImageUniforms}; + + Int counts[kStageCount] = {}; + const Int uniformCount = reflection.getNumUniformVariables(); + for (Int i = 0; i < uniformCount; ++i) { + const auto& uniform = reflection.getUniform(i); + const glslang::TType* type = uniform.getType(); + if (type == nullptr || !type->isImage()) continue; + // An array occupies one image unit per element; an unsized one (never indexed, so + // never more than the single element glslang kept) counts as one. + Int elements = uniform.size > 1 ? uniform.size : 1; + if (type->isArray()) { + elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1; + } + // `stages` is the set of stages that REFERENCE the uniform, which is exactly what GL + // counts: an image declared in two stages costs a unit in each, and one no stage + // reads is not active at all and costs nothing. + for (SizeT stage = 0; stage < kStageCount; ++stage) { + if ((static_cast(uniform.stages) & (1u << static_cast(kStages[stage]))) == 0) { + continue; + } + counts[stage] += elements; + } + } + + Int combined = 0; + for (SizeT stage = 0; stage < kStageCount; ++stage) { + combined += counts[stage]; + if (counts[stage] > limits[stage]) { + return std::format("This program uses {} active image uniforms in one stage, more than the {} " + "{} allows.", + counts[stage], limits[stage], kLimitNames[stage]); + } + } + if (combined > env.params.MaxCombinedImageUniforms) { + return std::format("This program uses {} active image uniforms across its stages, more than the {} " + "GL_MAX_COMBINED_IMAGE_UNIFORMS allows.", + combined, env.params.MaxCombinedImageUniforms); + } + return {}; + } + static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) { const auto* type = output.getType(); return type && type->getQualifier().builtIn != glslang::EbvNone; @@ -685,6 +760,14 @@ namespace MobileGL::MG_State::GLState { return false; } + if (String imageUniformError = ValidateImageUniformLimits(*artifacts.program, env); + !imageUniformError.empty()) { + artifacts.infoLog = Move(imageUniformError); + 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 e1e6a695..16430327 100644 --- a/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp @@ -754,6 +754,77 @@ void main() { 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 + // them - so every deliberately-oversized program in + // KHR-GL4x.shader_image_load_store.uniform-limits linked cleanly and then rendered nothing. + // + // Sized off the ADVERTISED limits rather than a constant, because the numbers come from the + // active backend and the whole point of the check is that the two agree. + TEST_F(ProgramInterfaceTest, ImageUniformsOverAStageLimitFailToLink) { + GLint maxFragmentImages = 0; + GLint maxCombinedImages = 0; + GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages); + GetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImages); + ClearErrors(); + ASSERT_GT(maxFragmentImages, 0); + + // The fragment stage is compiled explicitly so a COMPILE failure can never be mistaken + // for the link failure under test. + const auto linkWithFragmentImages = [](GLint count) { + const std::string n = std::to_string(count); + const std::string source = std::string(R"(#version 430 +out vec4 color; +layout(r32i) uniform iimage2D u_image[)") + n + R"(]; +void main() { + int value = 1; + for (int i = 0; i < )" + n + R"(; ++i) { + value = imageAtomicAdd(u_image[i], ivec2(0), value); + } + color = vec4(float(value)); +} +)"; + const char* sourcePtr = source.c_str(); + const GLuint fs = CreateShader(GL_FRAGMENT_SHADER); + ShaderSource(fs, 1, &sourcePtr, nullptr); + CompileShader(fs); + GLint compiled = 0; + GetShaderiv(fs, GL_COMPILE_STATUS, &compiled); + EXPECT_EQ(compiled, GL_TRUE) << "the fragment stage with " << count << " image uniforms must compile"; + + const GLuint vs = CreateShader(GL_VERTEX_SHADER); + ShaderSource(vs, 1, &kSimpleVs, nullptr); + CompileShader(vs); + + const GLuint program = CreateProgram(); + AttachShader(program, vs); + AttachShader(program, fs); + LinkProgram(program); + return program; + }; + + const GLuint over = linkWithFragmentImages(maxFragmentImages + 1); + 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_IMAGE_UNIFORMS"), std::string::npos) + << "info log was: " << log; + ClearErrors(); + + // Exactly AT the limit is legal and must still link: the comparison is strictly + // greater-than, and the conformance suite's combined-stage subcase builds a program that + // fills every stage to its own limit and expects it to link whenever the combined limit + // can hold them. + if (maxFragmentImages <= maxCombinedImages) { + const GLuint atLimit = linkWithFragmentImages(maxFragmentImages); + 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