From e0193f0b3d0c757a95c83414c8a5c494e0143c6e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 08:01:45 -0400 Subject: [PATCH] [Fix] (GL): fail the link of a non-separable program that has a tessellation or geometry stage but no vertex stage --- MobileGL/MG_State/GLState/Core.cpp | 3 ++ .../GLState/ProgramState/ProgramLinkTask.cpp | 22 ++++++++++++ .../GLState/ProgramState/ProgramLinkTask.h | 2 ++ .../GLState/ProgramState/ProgramObject.cpp | 1 + .../Program/ProgramPipelineCompositeTest.cpp | 34 +++++++++++++++++++ 5 files changed, 62 insertions(+) diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 2bf9f466..6f4d11fb 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -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); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 2f9945e9..b58e256c 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -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); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h index 40693b2c..58b3eeda 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h @@ -63,6 +63,8 @@ namespace MobileGL::MG_State::GLState { SharedPtr 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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 040a3705..f41c76cd 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -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; diff --git a/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp b/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp index ecf6fcde..1d9a7eae 100644 --- a/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp @@ -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. //