[Feat] (Program): report the geometry and tessellation link properties glGetProgramiv had no source for

This commit is contained in:
2026-08-27 03:49:38 -04:00
parent 7168f2ef77
commit 6cc9faf772
4 changed files with 300 additions and 2 deletions
@@ -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<GenericErrorInfo>("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<GLint>(programObject->GetGeometryInputType()); break;
case GL_GEOMETRY_OUTPUT_TYPE: *params = static_cast<GLint>(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<GenericErrorInfo>(
"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<GenericErrorInfo>(
"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<GLint>(programObject->GetTessGenMode()); break;
case GL_TESS_GEN_SPACING: *params = static_cast<GLint>(programObject->GetTessGenSpacing()); break;
case GL_TESS_GEN_VERTEX_ORDER:
*params = static_cast<GLint>(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(
@@ -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
@@ -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
+163
View File
@@ -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<std::pair<GLenum, const char*>>& 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<GLenum>(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<GLenum>(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);
}