diff --git a/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp b/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp index 94a9f6a5..ecf6fcde 100644 --- a/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp @@ -570,3 +570,155 @@ void main() { gl_Position = vec4(0.0); EmitVertex(); EndPrimitive(); } for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) { } } + +// --------------------------------------------------------------------------------------------- +// The composite's transform-feedback capture list. +// +// Two rules, and getting either wrong turns a working pipeline into one where EVERY draw reports +// GL_INVALID_OPERATION: an unresolvable capture name fails the composite's own link, and +// ValidateProgramForExecution rejects every draw through a pipeline whose composite did not link - +// while glValidateProgramPipeline keeps reporting TRUE. +// --------------------------------------------------------------------------------------------- + +namespace { + const char* kCaptureVs = R"(#version 430 core +out gl_PerVertex { vec4 gl_Position; }; +out float v_captured; +out float v_other; +void main() { gl_Position = vec4(0.0); v_captured = 1.0; v_other = 2.0; } +)"; + + // A geometry stage that re-emits nothing the vertex stage named, so a capture list taken from + // the VERTEX program cannot resolve against it. + const char* kPassthroughGs = R"(#version 430 core +layout(points) in; +layout(points, max_vertices = 1) out; +out gl_PerVertex { vec4 gl_Position; }; +out float g_only; +void main() { gl_Position = vec4(0.0); g_only = 1.0; EmitVertex(); EndPrimitive(); } +)"; + + Vector CompositeCaptureNames(MG_State::GLState::ProgramObject& composite) { + Vector names; + for (SizeT i = 0; i < composite.GetTransformFeedbackVaryingCount(); ++i) { + if (const auto* varying = composite.GetTransformFeedbackVarying(i)) { + names.push_back(varying->name); + } + } + return names; + } +} // namespace + +// glTransformFeedbackVaryings does not take effect until the program's NEXT link (GL 4.6 core +// 7.3/11.1.2.1) and deliberately bumps no version, so a request written after the stage program's +// last link is invisible to the composite cache's signature - yet the next rebuild would pick it +// up. The capture list would then depend on whether some unrelated event happened to invalidate +// the cache. Reading the LINKED snapshot removes the whole class, and makes the existing cache key +// sufficient: linked state only moves at a link, which is exactly what the key tracks. +TEST_F(ProgramPipelineCompositeTest, CompositeCaptureListComesFromTheLinkedSnapshotNotThePendingRequest) { + const GLuint vs = CreateProgram(); + { + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + ShaderSource(shader, 1, &kCaptureVs, nullptr); + CompileShader(shader); + ProgramParameteri(vs, GL_PROGRAM_SEPARABLE, GL_TRUE); + AttachShader(vs, shader); + const char* captured = "v_captured"; + TransformFeedbackVaryings(vs, 1, &captured, GL_INTERLEAVED_ATTRIBS); + LinkProgram(vs); + GLint linked = GL_FALSE; + GetProgramiv(vs, GL_LINK_STATUS, &linked); + ASSERT_EQ(linked, GL_TRUE); + } + const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs); + + GLuint pipeline = 0; + GenProgramPipelines(1, &pipeline); + BindProgramPipeline(pipeline); + UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs); + UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + { + const auto composite = DrawProgram(); + ASSERT_NE(composite, nullptr); + EXPECT_EQ(CompositeCaptureNames(*composite), (Vector{"v_captured"})); + } + + // A NEW request with no relink. GL says the program still captures v_captured. + const char* other = "v_other"; + TransformFeedbackVaryings(vs, 1, &other, GL_INTERLEAVED_ATTRIBS); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + // Force a composite rebuild through something entirely unrelated to the capture list: a new + // fragment stage program moves that slot's lifetime id, so the cache signature changes. + const GLuint fs2 = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs); + UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs2); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + { + const auto composite = DrawProgram(); + ASSERT_NE(composite, nullptr); + EXPECT_TRUE(composite->GetLinkStatus()) << "the composite must still link"; + EXPECT_EQ(CompositeCaptureNames(*composite), (Vector{"v_captured"})) + << "an unlinked request must not reach the composite"; + } + + // Relinking the stage program IS what makes the new request take effect - and the composite + // follows, because the relink moves the link version the cache keys on. + LinkProgram(vs); + { + const auto composite = DrawProgram(); + ASSERT_NE(composite, nullptr); + EXPECT_EQ(CompositeCaptureNames(*composite), (Vector{"v_other"})); + } + + BindProgramPipeline(0); + DeleteProgramPipelines(1, &pipeline); +} + +// Transform feedback captures the output of the LAST vertex-processing stage (GL 4.6 core +// 11.1.2.1) - the last stage that EXISTS, not the last one that happens to carry a capture list. +// Falling through a geometry stage with no request and installing the vertex stage's list instead +// made the two halves disagree: this loop picks whose list, the link task resolves those names +// against the geometry intermediate. Either it captures where GL says it must not, or the +// composite fails to link and every draw through the pipeline reports GL_INVALID_OPERATION. +TEST_F(ProgramPipelineCompositeTest, CompositeCaptureStageIsTheLastVertexProcessingStageThatExists) { + const GLuint vs = CreateProgram(); + { + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + ShaderSource(shader, 1, &kCaptureVs, nullptr); + CompileShader(shader); + ProgramParameteri(vs, GL_PROGRAM_SEPARABLE, GL_TRUE); + AttachShader(vs, shader); + const char* captured = "v_captured"; + TransformFeedbackVaryings(vs, 1, &captured, GL_INTERLEAVED_ATTRIBS); + LinkProgram(vs); + GLint linked = GL_FALSE; + GetProgramiv(vs, GL_LINK_STATUS, &linked); + ASSERT_EQ(linked, GL_TRUE); + } + // The geometry program was never given a capture list, and "v_captured" is not one of its + // outputs - so a composite seeded from the VERTEX program's list cannot resolve it. + const GLuint gs = MakeSeparableProgram(GL_GEOMETRY_SHADER, kPassthroughGs); + const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs); + + GLuint pipeline = 0; + GenProgramPipelines(1, &pipeline); + BindProgramPipeline(pipeline); + UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs); + UseProgramStages(pipeline, GL_GEOMETRY_SHADER_BIT, gs); + UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + const auto composite = DrawProgram(); + ASSERT_NE(composite, nullptr); + EXPECT_TRUE(composite->GetLinkStatus()) + << "the geometry stage is the capture stage and has no capture list, so the composite links " + "with none - it must not inherit the vertex stage's and fail resolving it"; + EXPECT_EQ(composite->GetTransformFeedbackVaryingCount(), 0u) + << "the capture stage is the geometry program, which declared nothing to capture"; + + BindProgramPipeline(0); + DeleteProgramPipelines(1, &pipeline); +} diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index aea4e8b2..ff3c38aa 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -4361,6 +4361,54 @@ TEST_F(ProgramTest, UniformEntryPointsRejectAnUnlinkedProgram) { ProgramUniform1f(program, 0, 1.0f); EXPECT_EQ(GetError(), GL_INVALID_OPERATION); + // THE MATRIX FORMS TOO. The reorder originally landed on ProgramUniformv_State alone, so all + // thirteen glProgramUniformMatrix* entry points kept the old `if (location == -1) return;` + // first statement and stayed silent on exactly the case the rule exists for. + const GLfloat m[16] = {}; + ProgramUniformMatrix2fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix2fv"; + ProgramUniformMatrix3fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix3fv"; + ProgramUniformMatrix4fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix4fv"; + ProgramUniformMatrix2x3fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix2x3fv"; + ProgramUniformMatrix3x2fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix3x2fv"; + ProgramUniformMatrix2x4fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix2x4fv"; + ProgramUniformMatrix4x2fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix4x2fv"; + ProgramUniformMatrix3x4fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix3x4fv"; + ProgramUniformMatrix4x3fv(program, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix4x3fv"; + + const GLdouble md[16] = {}; + ProgramUniformMatrix2dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix2dv"; + ProgramUniformMatrix3dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix3dv"; + ProgramUniformMatrix4dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix4dv"; + ProgramUniformMatrix2x3dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix2x3dv"; + ProgramUniformMatrix3x2dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix3x2dv"; + ProgramUniformMatrix2x4dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix2x4dv"; + ProgramUniformMatrix4x2dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix4x2dv"; + ProgramUniformMatrix3x4dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix3x4dv"; + ProgramUniformMatrix4x3dv(program, -1, 1, GL_FALSE, md); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glProgramUniformMatrix4x3dv"; + + // A name GL never handed out is INVALID_VALUE, and the -1 location must not swallow that + // either - this is the second error the early-out was hiding. + ProgramUniformMatrix4fv(0xDEADBEEFu, -1, 1, GL_FALSE, m); + EXPECT_EQ(GetError(), GL_INVALID_VALUE); + DrainProgramTestErrors(); }