From 53f3b7a9afdbfb156ec74f4dedfbc08d523967d3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 18:28:03 -0400 Subject: [PATCH] [Fix] (GL): leave the injected default fragment stage out of program pipeline validation --- .../GLState/ProgramState/ProgramObject.h | 11 +++++ .../ProgramState/ProgramPipelineObject.h | 2 +- .../Program/ProgramPipelineCompositeTest.cpp | 41 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index aca04f0e..29faf59b 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h index 8522e1c7..d328c072 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h @@ -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(other)) && + if (program->HasApplicationLinkedShaderStage(static_cast(other)) && m_stagePrograms[other] != program) { return std::format("A program is active for stage {} but not for its linked stage {}.", stage, other); diff --git a/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp b/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp index e3f7ab50..76fbda59 100644 --- a/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp @@ -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. //