diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 8572e2f4..e938a3f7 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -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(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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index f73cd7e6..0d37e5dc 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -1317,6 +1317,10 @@ namespace MobileGL::MG_State::GLState { Vector 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 diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index a268b1e6..1fa25eee 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #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{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(); +}