From 6cc9faf7723112c6c2871fcc86964f1767b66c8d Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 27 Aug 2026 02:18:17 -0400 Subject: [PATCH] [Feat] (Program): report the geometry and tessellation link properties glGetProgramiv had no source for --- .../MG_Impl/GLImpl/Program/GL_Program.cpp | 62 +++++++ .../GLState/ProgramState/ProgramLinkTask.cpp | 48 +++++- .../GLState/ProgramState/ProgramObject.h | 29 ++++ MobileGL/MG_Test/Program/ProgramTest.cpp | 163 ++++++++++++++++++ 4 files changed, 300 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index fa237f13..8a53cc0e 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -747,9 +747,71 @@ namespace MobileGL::MG_Impl::GLImpl { *params = programObject->GetSeparable() ? GL_TRUE : GL_FALSE; break; + // The geometry and tessellation link properties (GL 4.6 core table 23.35). Same shape as + // GL_COMPUTE_WORK_GROUP_SIZE above, and for the same reason: "a linked program object + // with a geometry shader" is one whose EXECUTABLE has the stage, so an + // attached-but-not-yet-linked shader must give INVALID_OPERATION rather than the previous + // link's value. The geometry three used to be listed here only to fall through into the + // INVALID_ENUM default, and the tessellation five were not listed at all. case GL_GEOMETRY_VERTICES_OUT: case GL_GEOMETRY_INPUT_TYPE: case GL_GEOMETRY_OUTPUT_TYPE: + case GL_GEOMETRY_SHADER_INVOCATIONS: { + if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Geometry)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + std::to_string(program) + + " is not a linked program object with a geometry shader.")); + return; + } + switch (pname) { + case GL_GEOMETRY_VERTICES_OUT: *params = programObject->GetGeometryVerticesOut(); break; + case GL_GEOMETRY_INPUT_TYPE: *params = static_cast(programObject->GetGeometryInputType()); break; + case GL_GEOMETRY_OUTPUT_TYPE: *params = static_cast(programObject->GetGeometryOutputType()); break; + default: *params = programObject->GetGeometryShaderInvocations(); break; + } + MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params); + break; + } + case GL_TESS_CONTROL_OUTPUT_VERTICES: { + if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessControl)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", __func__, + std::to_string(program) + + " is not a linked program object with a tessellation control shader.")); + return; + } + *params = programObject->GetTessControlOutputVertices(); + MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params); + break; + } + case GL_TESS_GEN_MODE: + case GL_TESS_GEN_SPACING: + case GL_TESS_GEN_VERTEX_ORDER: + case GL_TESS_GEN_POINT_MODE: { + if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessEval)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", __func__, + std::to_string(program) + + " is not a linked program object with a tessellation evaluation shader.")); + return; + } + switch (pname) { + case GL_TESS_GEN_MODE: *params = static_cast(programObject->GetTessGenMode()); break; + case GL_TESS_GEN_SPACING: *params = static_cast(programObject->GetTessGenSpacing()); break; + case GL_TESS_GEN_VERTEX_ORDER: + *params = static_cast(programObject->GetTessGenVertexOrder()); + break; + default: *params = programObject->GetTessGenPointMode() ? GL_TRUE : GL_FALSE; break; + } + MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params); + break; + } default: MGLOG_D("%s: %s", __func__, MG_Util::ConvertGLEnumToString(pname).c_str()); MG_State::pGLContext->RecordError( diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index e4e15a7e..67aa2f1d 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -670,9 +670,14 @@ namespace MobileGL::MG_State::GLState { return; } - // GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it - // (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one. + // The geometry stage's link properties. GL_GEOMETRY_INPUT_TYPE is load-bearing beyond the + // query surface - a draw's primitive type has to be compatible with it (GL 4.6 core + // 11.3.1) - so this block runs for every link, not only a capturing one. The other three + // are pure glGetProgramiv answers that previously had no source at all. artifacts.gsInputPrimitive = GL_NONE; + artifacts.gsOutputPrimitive = GL_NONE; + artifacts.gsMaxVertices = 0; + artifacts.gsInvocations = 0; if (const glslang::TIntermediate* gs = artifacts.program->getIntermediate(EShLangGeometry)) { switch (gs->getInputPrimitive()) { case glslang::ElgPoints: artifacts.gsInputPrimitive = GL_POINTS; break; @@ -682,6 +687,45 @@ namespace MobileGL::MG_State::GLState { case glslang::ElgTrianglesAdjacency: artifacts.gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break; default: break; } + switch (gs->getOutputPrimitive()) { + case glslang::ElgPoints: artifacts.gsOutputPrimitive = GL_POINTS; break; + case glslang::ElgLineStrip: artifacts.gsOutputPrimitive = GL_LINE_STRIP; break; + case glslang::ElgTriangleStrip: artifacts.gsOutputPrimitive = GL_TRIANGLE_STRIP; break; + default: break; + } + // glslang leaves both at TQualifier::layoutNotSet (-1) when the shader declared no + // such layout, and `invocations` defaults to one per GLSL 4.60 4.4.2.2 - so clamp + // rather than forward, or GL_GEOMETRY_SHADER_INVOCATIONS reports the sentinel. + artifacts.gsMaxVertices = std::max(gs->getVertices(), 0); + artifacts.gsInvocations = std::max(gs->getInvocations(), 1); + } + + // The tessellation evaluation stage's link properties, GL 4.6 core table 23.35: the + // primitive generator's mode, spacing, winding and point mode. (The control stage's + // output patch size is captured below, together with the limit check that goes with it.) + artifacts.tessGenMode = GL_NONE; + artifacts.tessGenSpacing = GL_NONE; + artifacts.tessGenVertexOrder = GL_NONE; + artifacts.tessGenPointMode = false; + if (const glslang::TIntermediate* tes = artifacts.program->getIntermediate(EShLangTessEvaluation)) { + switch (tes->getInputPrimitive()) { + case glslang::ElgTriangles: artifacts.tessGenMode = GL_TRIANGLES; break; + case glslang::ElgQuads: artifacts.tessGenMode = GL_QUADS; break; + case glslang::ElgIsolines: artifacts.tessGenMode = GL_ISOLINES; break; + default: break; + } + // GLSL 4.60 4.4.2.3: equal_spacing and ccw are the defaults, which is what an unset + // qualifier means here. + switch (tes->getVertexSpacing()) { + case glslang::EvsFractionalEven: artifacts.tessGenSpacing = GL_FRACTIONAL_EVEN; break; + case glslang::EvsFractionalOdd: artifacts.tessGenSpacing = GL_FRACTIONAL_ODD; break; + default: artifacts.tessGenSpacing = GL_EQUAL; break; + } + switch (tes->getVertexOrder()) { + case glslang::EvoCw: artifacts.tessGenVertexOrder = GL_CW; break; + default: artifacts.tessGenVertexOrder = GL_CCW; break; + } + artifacts.tessGenPointMode = tes->getPointMode(); } // GL_TESS_CONTROL_OUTPUT_VERTICES, i.e. the `layout(vertices = N) out` the control stage diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index f991a6ba..08d81e0c 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -1356,6 +1356,23 @@ namespace MobileGL::MG_State::GLState { // 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; + // The rest of the geometry stage's link properties, and the tessellation evaluation + // stage's. Every one of these is a glGetProgramiv answer that had no source at all: + // the query surface listed the geometry pnames only to fall through to + // GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They + // come from the linked intermediates for the same reason gsInputPrimitive and + // tcsOutputVertices do - glslang has already merged the compilation units' layout + // qualifiers and diagnosed contradictions, so the linked program is the thing that + // knows. + GLenum gsOutputPrimitive = GL_NONE; + Int gsMaxVertices = 0; + Int gsInvocations = 0; + // The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES, + // GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode. + GLenum tessGenMode = GL_NONE; + GLenum tessGenSpacing = GL_NONE; + GLenum tessGenVertexOrder = GL_NONE; + Bool tessGenPointMode = false; GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS; Int xfbVaryingNameMaxLength = 0; Bool xfbNeedsScatteredCapture = false; @@ -1545,10 +1562,22 @@ 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_GEOMETRY_OUTPUT_TYPE (GL_POINTS, GL_LINE_STRIP or GL_TRIANGLE_STRIP), + // GL_GEOMETRY_VERTICES_OUT and GL_GEOMETRY_SHADER_INVOCATIONS of the linked geometry + // stage. Meaningless without one - glGetProgramiv raises INVALID_OPERATION there. + GLenum GetGeometryOutputType() const { return Artifacts().gsOutputPrimitive; } + Int GetGeometryVerticesOut() const { return Artifacts().gsMaxVertices; } + Int GetGeometryShaderInvocations() const { return Artifacts().gsInvocations; } // 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; } + // GL_TESS_GEN_MODE / _SPACING / _VERTEX_ORDER / _POINT_MODE of the linked tessellation + // evaluation stage. + GLenum GetTessGenMode() const { return Artifacts().tessGenMode; } + GLenum GetTessGenSpacing() const { return Artifacts().tessGenSpacing; } + GLenum GetTessGenVertexOrder() const { return Artifacts().tessGenVertexOrder; } + Bool GetTessGenPointMode() const { return Artifacts().tessGenPointMode; } 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 1fcf696e..b42c40cd 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -3973,3 +3973,166 @@ void main() { o_color = vec4(1.0); } EXPECT_FALSE(programObject->WriteReservedNumSamples(4)); EXPECT_EQ(GetError(), GL_NO_ERROR); } + +// glGetProgramiv's geometry and tessellation link properties (GL 4.6 core table 23.35). None of +// these had a source: GL_GEOMETRY_VERTICES_OUT / _INPUT_TYPE / _OUTPUT_TYPE were listed in the +// switch only to fall through into the GL_INVALID_ENUM default, GL_GEOMETRY_SHADER_INVOCATIONS +// and the five GL_TESS_* pnames were not listed at all, and the link recorded nothing but the +// geometry INPUT primitive. 72 of the tessellation family's 116 failing conformance bodies died +// on the first of these queries, before touching a single tessellation feature. +namespace { + GLuint CompileStage(GLenum type, const char* source) { + const GLuint shader = CreateShader(type); + ShaderSource(shader, 1, &source, nullptr); + CompileShader(shader); + GLint status = GL_FALSE; + GetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (status != GL_TRUE) { + char infoLog[2048] = ""; + GetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog); + ADD_FAILURE() << "stage " << type << " failed to compile: " << infoLog; + } + return shader; + } + + GLuint LinkStages(const std::vector>& stages) { + const GLuint program = CreateProgram(); + for (const auto& [type, source] : stages) { + const GLuint shader = CompileStage(type, source); + AttachShader(program, shader); + DeleteShader(shader); + } + LinkProgram(program); + GLint linkStatus = GL_FALSE; + GetProgramiv(program, GL_LINK_STATUS, &linkStatus); + if (linkStatus != GL_TRUE) { + char infoLog[2048] = ""; + GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog); + ADD_FAILURE() << "link failed: " << infoLog; + } + return program; + } + + constexpr const char* kPassthroughVs = R"(#version 460 core +void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); } +)"; + constexpr const char* kPassthroughFs = R"(#version 460 core +out vec4 mgColor; +void main() { mgColor = vec4(1.0); } +)"; +} // namespace + +TEST_F(ProgramTest, GetProgramivReportsTheGeometryStageLinkProperties) { + constexpr const char* gs = R"(#version 460 core +layout(triangles, invocations = 3) in; +layout(line_strip, max_vertices = 7) out; +void main() { + for (int i = 0; i < 3; ++i) { gl_Position = gl_in[i].gl_Position; EmitVertex(); } + EndPrimitive(); +} +)"; + const GLuint program = + LinkStages({{GL_VERTEX_SHADER, kPassthroughVs}, {GL_GEOMETRY_SHADER, gs}, {GL_FRAGMENT_SHADER, kPassthroughFs}}); + + GLint value = -1; + GetProgramiv(program, GL_GEOMETRY_INPUT_TYPE, &value); + EXPECT_EQ(value, GL_TRIANGLES); + GetProgramiv(program, GL_GEOMETRY_OUTPUT_TYPE, &value); + EXPECT_EQ(value, GL_LINE_STRIP); + GetProgramiv(program, GL_GEOMETRY_VERTICES_OUT, &value); + EXPECT_EQ(value, 7); + GetProgramiv(program, GL_GEOMETRY_SHADER_INVOCATIONS, &value); + EXPECT_EQ(value, 3); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + // ...and INVALID_OPERATION, not INVALID_ENUM, on a program that has no geometry stage: GL + // says "a linked program object with a geometry shader", which the conformance suite checks + // from both sides. + const GLuint noGeometry = LinkStages({{GL_VERTEX_SHADER, kPassthroughVs}, {GL_FRAGMENT_SHADER, kPassthroughFs}}); + for (const GLenum pname : {GL_GEOMETRY_INPUT_TYPE, GL_GEOMETRY_OUTPUT_TYPE, GL_GEOMETRY_VERTICES_OUT, + GL_GEOMETRY_SHADER_INVOCATIONS}) { + GetProgramiv(noGeometry, pname, &value); + EXPECT_EQ(GetError(), static_cast(GL_INVALID_OPERATION)) << "pname " << pname; + } +} + +TEST_F(ProgramTest, GetProgramivReportsTheTessellationStageLinkProperties) { + constexpr const char* tcs = R"(#version 460 core +layout(vertices = 3) out; +void main() { + gl_TessLevelOuter[0] = 1.0; gl_TessLevelOuter[1] = 1.0; gl_TessLevelOuter[2] = 1.0; + gl_TessLevelInner[0] = 1.0; + gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; +} +)"; + constexpr const char* tes = R"(#version 460 core +layout(quads, fractional_odd_spacing, cw, point_mode) in; +void main() { gl_Position = gl_in[0].gl_Position; } +)"; + const GLuint program = LinkStages({{GL_VERTEX_SHADER, kPassthroughVs}, + {GL_TESS_CONTROL_SHADER, tcs}, + {GL_TESS_EVALUATION_SHADER, tes}, + {GL_FRAGMENT_SHADER, kPassthroughFs}}); + + GLint value = -1; + GetProgramiv(program, GL_TESS_CONTROL_OUTPUT_VERTICES, &value); + EXPECT_EQ(value, 3); + GetProgramiv(program, GL_TESS_GEN_MODE, &value); + EXPECT_EQ(value, GL_QUADS); + GetProgramiv(program, GL_TESS_GEN_SPACING, &value); + EXPECT_EQ(value, GL_FRACTIONAL_ODD); + GetProgramiv(program, GL_TESS_GEN_VERTEX_ORDER, &value); + EXPECT_EQ(value, GL_CW); + GetProgramiv(program, GL_TESS_GEN_POINT_MODE, &value); + EXPECT_EQ(value, GL_TRUE); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + // GLSL 4.60 4.4.2.3 defaults: equal_spacing, ccw, no point mode. + constexpr const char* defaultTes = R"(#version 460 core +layout(triangles) in; +void main() { gl_Position = gl_in[0].gl_Position; } +)"; + const GLuint defaults = LinkStages({{GL_VERTEX_SHADER, kPassthroughVs}, + {GL_TESS_CONTROL_SHADER, tcs}, + {GL_TESS_EVALUATION_SHADER, defaultTes}, + {GL_FRAGMENT_SHADER, kPassthroughFs}}); + GetProgramiv(defaults, GL_TESS_GEN_MODE, &value); + EXPECT_EQ(value, GL_TRIANGLES); + GetProgramiv(defaults, GL_TESS_GEN_SPACING, &value); + EXPECT_EQ(value, GL_EQUAL); + GetProgramiv(defaults, GL_TESS_GEN_VERTEX_ORDER, &value); + EXPECT_EQ(value, GL_CCW); + GetProgramiv(defaults, GL_TESS_GEN_POINT_MODE, &value); + EXPECT_EQ(value, GL_FALSE); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + const GLuint noTess = LinkStages({{GL_VERTEX_SHADER, kPassthroughVs}, {GL_FRAGMENT_SHADER, kPassthroughFs}}); + for (const GLenum pname : {GL_TESS_CONTROL_OUTPUT_VERTICES, GL_TESS_GEN_MODE, GL_TESS_GEN_SPACING, + GL_TESS_GEN_VERTEX_ORDER, GL_TESS_GEN_POINT_MODE}) { + GetProgramiv(noTess, pname, &value); + EXPECT_EQ(GetError(), static_cast(GL_INVALID_OPERATION)) << "pname " << pname; + } +} + +// The context-wide tessellation state the same conformance group reads before it links anything. +// glGetBooleanv and glGetFloatv both have to answer GL_PATCH_DEFAULT_OUTER_LEVEL, which is +// FLOAT state - a delegation that writes element 0 only would leave the other three components +// as whatever was in the caller's stack. +TEST_F(ProgramTest, ContextWideTessellationPropertiesAnswerEveryWidth) { + GLfloat outer[4] = {-1.0f, -1.0f, -1.0f, -1.0f}; + GetFloatv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer); + for (const GLfloat level : outer) EXPECT_FLOAT_EQ(level, 1.0f); + + GLfloat inner[2] = {-1.0f, -1.0f}; + GetFloatv(GL_PATCH_DEFAULT_INNER_LEVEL, inner); + for (const GLfloat level : inner) EXPECT_FLOAT_EQ(level, 1.0f); + + GLboolean outerBools[4] = {GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE}; + GetBooleanv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerBools); + for (const GLboolean level : outerBools) EXPECT_EQ(level, GL_TRUE); + + GLint restart = -1; + GetIntegerv(GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED, &restart); + EXPECT_EQ(restart, GL_FALSE); + EXPECT_EQ(GetError(), GL_NO_ERROR); +}