[Fix] (GL): leave the injected default fragment stage out of program pipeline validation

This commit is contained in:
2026-10-12 18:28:03 -04:00
parent 47601cbbb8
commit 53f3b7a9af
3 changed files with 53 additions and 1 deletions
@@ -124,6 +124,17 @@ namespace MobileGL::MG_State::GLState {
return ref.shader && ref.shader->GetShaderStage() == stage;
});
}
// HasLinkedShaderStage without the default fragment shader Link() adds to a vertex-only
// program on a backend that cannot run one (AddDefaultFragmentShaderIfMissing; its shader
// object has no name). That stage is MobileGL's, not the application's, so it is not one
// the GL rules about "the stages present when the program was linked" may count.
Bool HasApplicationLinkedShaderStage(ShaderStage stage) const {
return std::any_of(m_linkedShaderSnapshot.begin(), m_linkedShaderSnapshot.end(),
[stage](const LinkedShaderRef& ref) {
return ref.shader && ref.shader->GetShaderStage() == stage &&
ref.shader->GetExternalIndex() != 0;
});
}
// The stage of each module of GetGeneratedSpirv(), at the SAME index and with the same
// size: phase B emits exactly one module per entry of the snapshot above, in that order
// (Link() fills ProgramLinkTask::in.shaders from the snapshot loop, phase A copies the
@@ -74,7 +74,7 @@ namespace MobileGL {
// "A program object is active for at least one, but not all of the shader
// stages that were present when the program was linked."
for (SizeT other = 0; other < kStageCount; ++other) {
if (program->HasLinkedShaderStage(static_cast<ShaderStage>(other)) &&
if (program->HasApplicationLinkedShaderStage(static_cast<ShaderStage>(other)) &&
m_stagePrograms[other] != program) {
return std::format("A program is active for stage {} but not for its linked stage {}.",
stage, other);
@@ -673,6 +673,47 @@ void main() { gl_Position = vec4(0.0); }
}
}
// KHR-GL4x.geometry_shader.api.program_pipeline_vs_gs_capture: a vertex-only separable program
// and a geometry-only one. Link() gives the vertex-only program MobileGL's default fragment shader
// (neither backend runs a vertex-only program), and the "active for some but not all of its linked
// stages" rule must not count that stage, or every such pipeline fails validation and its draws.
TEST_F(ProgramPipelineCompositeTest, AVertexOnlyStageProgramValidatesWithoutAFragmentStage) {
const char* kVs = R"(#version 430 core
out gl_PerVertex { vec4 gl_Position; };
flat out int vertexID;
void main() { vertexID = gl_VertexID; gl_Position = vec4(0.0); }
)";
const char* kGs = R"(#version 430 core
in gl_PerVertex { vec4 gl_Position; } gl_in[];
out gl_PerVertex { vec4 gl_Position; };
layout(points) in;
layout(points, max_vertices = 1) out;
flat in int vertexID[];
out vec4 color;
void main() { color = vec4(vertexID[0]); gl_Position = vec4(0.0); EmitVertex(); }
)";
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kVs);
const GLuint gs = MakeSeparableProgram(GL_GEOMETRY_SHADER, kGs);
GLuint pipeline = 0;
GenProgramPipelines(1, &pipeline);
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
UseProgramStages(pipeline, GL_GEOMETRY_SHADER_BIT, gs);
BindProgramPipeline(pipeline);
ValidateProgramPipeline(pipeline);
GLint status = -1;
GetProgramPipelineiv(pipeline, GL_VALIDATE_STATUS, &status);
EXPECT_EQ(status, GL_TRUE);
ASSERT_EQ(GetError(), GL_NO_ERROR);
DrawArrays(GL_POINTS, 0, 1);
EXPECT_EQ(GetError(), GL_NO_ERROR) << "a draw through the valid pipeline";
BindProgramPipeline(0);
DeleteProgramPipelines(1, &pipeline);
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
}
}
// ---------------------------------------------------------------------------------------------
// The composite's transform-feedback capture list.
//