mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Fix, Test] (GLState): fail the link when a stage exceeds GL_MAX_*_IMAGE_UNIFORMS
This commit is contained in:
@@ -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<unsigned>(uniform.stages) & (1u << static_cast<unsigned>(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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user