[Fix] (GL): fail the link of a non-separable program that has a tessellation or geometry stage but no vertex stage

This commit is contained in:
2026-10-12 08:32:48 -04:00
parent 35077242c3
commit e0193f0b3d
5 changed files with 62 additions and 0 deletions
+3
View File
@@ -823,6 +823,9 @@ namespace MobileGL::MG_State {
}
break;
}
// Separable, as every program it is assembled from is: a pipeline may leave the vertex
// stage to nobody, and that is the draw-time pipeline rule's to refuse, not the link's.
composite->SetSeparable(true);
// A pipeline with no fragment stage still rasterises, so the default fragment
// shader is wanted here even though the separable stage programs never get one.
composite->Link(true);
@@ -1223,6 +1223,28 @@ namespace MobileGL::MG_State::GLState {
}
}
// GL 4.6 core 7.3: a program that is NOT separable and has a tessellation control,
// tessellation evaluation or geometry shader must also have a vertex shader - only a
// separable program may supply a subset of the vertex-processing stages and leave the
// rest to a program pipeline. Pipeline composites link as separable for the same reason;
// the draw-time pipeline rule is what refuses such a composite.
if (!in.separable) {
Bool hasVertex = false;
Bool hasLaterVertexProcessing = false;
for (const LinkShaderInput& input : in.shaders) {
hasVertex = hasVertex || input.stage == ShaderStage::Vertex;
hasLaterVertexProcessing = hasLaterVertexProcessing || input.stage == ShaderStage::TessControl ||
input.stage == ShaderStage::TessEval ||
input.stage == ShaderStage::Geometry;
}
if (hasLaterVertexProcessing && !hasVertex) {
artifacts.infoLog = "A program that is not separable and has a tessellation or geometry shader "
"must also have a vertex shader.";
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
return false;
}
}
for (SizeT i = 0; i < in.shaders.size(); i++) {
const LinkShaderInput& input = in.shaders[i];
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
@@ -63,6 +63,8 @@ namespace MobileGL::MG_State::GLState {
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
// Startup configuration copied with the task, never read from worker code.
Bool enableSpirvValidation = false;
// GL_PROGRAM_SEPARABLE as this link latches it (GL 4.6 core 7.3).
Bool separable = false;
// The four "takes effect at the next link" request maps. Snapshotted rather than
// referenced, which is precisely what makes glBindAttribLocation and friends
// legal to call over a pending link without cancelling it: the pending link keeps
@@ -617,6 +617,7 @@ namespace MobileGL::MG_State::GLState {
task->in.externalIndex = m_externalIndex;
task->in.env = MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
task->in.enableSpirvValidation = MG_Config::Features.EnableSpirvValidation;
task->in.separable = m_linkedSeparable;
task->in.explicitAttribLocations = m_explicitAttribLocations;
task->in.explicitFragDataLocation = m_explicitFragDataLocation;
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
@@ -571,6 +571,40 @@ void main() { gl_Position = vec4(0.0); EmitVertex(); EndPrimitive(); }
}
}
// GL 4.6 core 7.3: only a SEPARABLE program may have a geometry (or tessellation) stage without a
// vertex stage - KHR-GL4x.geometry_shader.linking.incomplete_program_objects. The pipeline
// composite assembled from such a separable program has to keep linking, which the case above
// already pins.
TEST_F(ProgramPipelineCompositeTest, OnlyASeparableProgramMayLinkAGeometryStageWithoutAVertexStage) {
const char* kGs = R"(#version 430 core
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_Position = vec4(0.0); EmitVertex(); EndPrimitive(); }
)";
const auto link = [&](Bool separable, Bool withFragment) {
const GLuint program = CreateProgram();
if (separable) ProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
const auto attach = [program](GLenum stage, const char* source) {
const GLuint shader = CreateShader(stage);
ShaderSource(shader, 1, &source, nullptr);
CompileShader(shader);
AttachShader(program, shader);
};
attach(GL_GEOMETRY_SHADER, kGs);
if (withFragment) attach(GL_FRAGMENT_SHADER, kSharedUniformFs);
LinkProgram(program);
GLint linked = -1;
GetProgramiv(program, GL_LINK_STATUS, &linked);
DeleteProgram(program);
return linked == GL_TRUE;
};
EXPECT_FALSE(link(false, false));
EXPECT_FALSE(link(false, true));
EXPECT_TRUE(link(true, false));
EXPECT_TRUE(link(true, true));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------------------
// The composite's transform-feedback capture list.
//