[Fix] (ProgramLink): fail the link when a tessellation control stage declares more output vertices than GL_MAX_PATCH_VERTICES

This commit is contained in:
Swung0x48
2026-08-27 02:11:23 -04:00
parent b675e2a0b0
commit e69e939d1a
3 changed files with 122 additions and 0 deletions
@@ -686,6 +686,38 @@ namespace MobileGL::MG_State::GLState {
}
}
// GL_TESS_CONTROL_OUTPUT_VERTICES, i.e. the `layout(vertices = N) out` the control stage
// declared, and the limit that goes with it.
//
// GL 4.6 core 11.2.1.1: the LINK fails when N is greater than MAX_PATCH_VERTICES. Nothing
// enforced it - glslang's layout handling only rejects N <= 0 (ParseHelper.cpp "must be
// greater than 0") and carries maxPatchVertices in TBuiltInResource purely so
// gl_MaxPatchVertices can expand from it, exactly the gap ValidateImageUniformLimits
// documents for image uniforms. Checked at LINK rather than at compile on purpose: the CTS
// requires the offending shader to COMPILE ("Compilation passed as allowed") and only the
// link to fail, and turning it into a parse error would newly break an application that
// compiles such a shader and never links it.
//
// The limit is the one glGetIntegerv answers (GL_Getter.cpp reads the same
// DynamicBackendParameters field), so the advertised number and the enforced number cannot
// drift apart.
artifacts.tcsOutputVertices = 0;
if (const glslang::TIntermediate* tcs = artifacts.program->getIntermediate(EShLangTessControl)) {
artifacts.tcsOutputVertices = static_cast<Int>(tcs->getVertices());
if (artifacts.tcsOutputVertices > env.params.MaxPatchVertices) {
artifacts.linkStatus = false;
// Same invariant as the compute local-size gate above: a rejected link leaves no
// TProgram behind for a query surface to find.
artifacts.program.reset();
artifacts.infoLog = std::format(
"Tessellation control shader declares an output patch of {} vertices, more than the {} "
"GL_MAX_PATCH_VERTICES allows.",
artifacts.tcsOutputVertices, env.params.MaxPatchVertices);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
return;
}
}
// ---- everything below this line up to GenerateSpirv() is the GL query surface ----
//
// ORDERING NOTE (rewritten 2026-08-10; the constraint it records was RETESTED, not
@@ -1317,6 +1317,10 @@ namespace MobileGL::MG_State::GLState {
Vector<Uint32> gsStripTriangles;
Bool gsStripCaptureFixup = false;
GLenum gsInputPrimitive = GL_NONE;
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
// tessellation control stage, or 0 when the program has none. Checked against
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
Int tcsOutputVertices = 0;
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int xfbVaryingNameMaxLength = 0;
Bool xfbNeedsScatteredCapture = false;
@@ -1501,6 +1505,10 @@ namespace MobileGL::MG_State::GLState {
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
// program has no geometry stage. Draws must present a compatible primitive type.
GLenum GetGeometryInputType() const { return Artifacts().gsInputPrimitive; }
// GL_TESS_CONTROL_OUTPUT_VERTICES of the linked tessellation control stage, or 0 when
// the program has no such stage. Never greater than GL_MAX_PATCH_VERTICES: a program
// that declared more does not link at all (GL 4.6 core 11.2.1.1).
Int GetTessControlOutputVertices() const { return Artifacts().tcsOutputVertices; }
Uint GetExternalIndex() const { return m_externalIndex; }
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
+82
View File
@@ -9,6 +9,7 @@
#include <gtest/gtest.h>
#include <spirv_reflect.h>
#include <cstring>
#include <utility>
#include <vector>
#include "Includes.h"
@@ -31,6 +32,13 @@ protected:
void SetUp() override { MobileGL::Initialize(); }
void TearDown() override {}
// GL error flags are sticky per code and the context outlives an individual test in this
// binary, so a pending error would be handed to whoever runs next.
static void DrainProgramTestErrors() {
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
}
}
};
TEST_F(ProgramTest, Sanity) {
@@ -3809,3 +3817,77 @@ TEST_F(ProgramTest, ImplicitLocationStaysInRangeWhenABufferBlockSharesTheProgram
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// --- `layout(vertices = N) out` against GL_MAX_PATCH_VERTICES -----------------------------------
//
// GL 4.6 core 11.2.1.1 makes N > MAX_PATCH_VERTICES a LINK failure. Nothing enforced it: glslang
// only rejects N <= 0, and carries maxPatchVertices in TBuiltInResource purely so
// gl_MaxPatchVertices can expand from it. The check deliberately lives at link and not at compile,
// because KHR-GL4x.tessellation_shader.compilation_and_linking_errors.
// tc_invalid_output_patch_vertex_count requires the shader to COMPILE ("Compilation passed as
// allowed") and only the program to fail.
TEST_F(ProgramTest, TessControlOutputPatchSizePastTheLimitFailsToLinkButStillCompiles) {
GLint maxPatchVertices = 0;
GetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
ASSERT_GT(maxPatchVertices, 0);
ASSERT_EQ(GetError(), GL_NO_ERROR);
const char* kVs = R"(#version 460 core
void main() { gl_Position = vec4(0.0); }
)";
const char* kTes = R"(#version 460 core
layout(triangles, equal_spacing, cw) in;
void main() { gl_Position = gl_in[0].gl_Position; }
)";
const char* kTcsPrologue = R"(#version 460 core
layout(vertices = )";
const char* kTcsEpilogue = R"() out;
void main() {
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
gl_TessLevelOuter[0] = 1.0;
}
)";
const auto buildWith = [&](const GLint vertices) {
const String tcs = String(kTcsPrologue) + std::to_string(vertices) + kTcsEpilogue;
const char* tcsSource = tcs.c_str();
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &kVs, nullptr);
CompileShader(vs);
const GLuint tc = CreateShader(GL_TESS_CONTROL_SHADER);
ShaderSource(tc, 1, &tcsSource, nullptr);
CompileShader(tc);
const GLuint te = CreateShader(GL_TESS_EVALUATION_SHADER);
ShaderSource(te, 1, &kTes, nullptr);
CompileShader(te);
// The offending stage COMPILES; only the link is allowed to notice.
GLint tcCompiled = GL_FALSE;
GetShaderiv(tc, GL_COMPILE_STATUS, &tcCompiled);
EXPECT_EQ(tcCompiled, GL_TRUE) << "vertices=" << vertices << " must still compile";
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, tc);
AttachShader(program, te);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
char infoLog[1024] = "";
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
return std::pair<GLint, String>{linkStatus, String(infoLog)};
};
const auto atTheLimit = buildWith(maxPatchVertices);
EXPECT_EQ(atTheLimit.first, GL_TRUE)
<< "exactly GL_MAX_PATCH_VERTICES is legal: " << atTheLimit.second;
const auto pastTheLimit = buildWith(maxPatchVertices + 1);
EXPECT_EQ(pastTheLimit.first, GL_FALSE) << "one past GL_MAX_PATCH_VERTICES must not link";
EXPECT_NE(pastTheLimit.second.find("GL_MAX_PATCH_VERTICES"), String::npos)
<< "the info log must name the limit it broke: " << pastTheLimit.second;
DrainProgramTestErrors();
}