diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index ceaf42fb..fe5cc80b 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -199,11 +199,37 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } + const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); + + // GL 4.6 core 10.1: the tessellation pipeline's only input primitive is GL_PATCHES, and + // GL_PATCHES has no meaning without it. Both directions are INVALID_OPERATION, and + // neither was implemented - which is two of the four sites + // KHR-GL43.transform_feedback.api_errors_test checks with one shared message string. + // The EVALUATION stage is what decides: a control stage cannot run without one, and a + // program carrying only an evaluation stage still tessellates, through GL's + // fixed-function pass-through control stage (11.2.2). + const Bool tessellationActive = + currentProgram && currentProgram->GetShaderIndexByStage(ShaderStage::TessEval) >= 0; + if (tessellationActive && mode != GL_PATCHES) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", functionName, + "A program with a tessellation evaluation shader can only be drawn with GL_PATCHES.")); + return false; + } + if (!tessellationActive && mode == GL_PATCHES) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", functionName, + "GL_PATCHES requires an active tessellation evaluation shader.")); + return false; + } + // A geometry stage only accepts the primitive types that decompose into its declared // input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES // is the tessellation pipeline's input and reaches the geometry stage already // converted, so it is not constrained here. - const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE; if (gsInput != GL_NONE && mode != GL_PATCHES) { Bool compatible = false; @@ -239,13 +265,17 @@ namespace MobileGL::MG_Impl::GLImpl { // While transform feedback is active the draw's primitive type must match // the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader // the constraint moves to the shader's output primitive type instead, so - // the draw mode itself is unconstrained here. A paused span is exempt: it - // captures nothing, so there is nothing for the mode to be incompatible with - // (GL 4.6 core 13.2.3). + // the draw mode itself is unconstrained here - and a TESSELLATION EVALUATION + // stage relocates it exactly the same way (GL 4.6 core 13.2.2 names both): + // what is captured is the tessellator's output primitive, and the draw mode + // can only ever be GL_PATCHES. A paused span is exempt: it captures nothing, + // so there is nothing for the mode to be incompatible with (GL 4.6 core 13.2.3). + const auto& feedbackProgram = MG_State::pGLContext->GetTransformFeedbackProgram(); + const Bool feedbackModeIsProgramDriven = + feedbackProgram && (feedbackProgram->GetShaderIndexByStage(ShaderStage::Geometry) >= 0 || + feedbackProgram->GetShaderIndexByStage(ShaderStage::TessEval) >= 0); if (MG_State::pGLContext->IsTransformFeedbackActive() && - !MG_State::pGLContext->IsTransformFeedbackPaused() && - !(MG_State::pGLContext->GetTransformFeedbackProgram() && - MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) { + !MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) { const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode(); Bool compatible = false; switch (feedbackMode) { diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index b995414c..e14dc712 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -85,6 +85,7 @@ add_executable(MobileGLIntegrationTest Scenarios/SsboDeclarationFormScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp Scenarios/IoBlockNameCollisionScenario.cpp + Scenarios/TessellationDrawModeScenario.cpp Scenarios/FragmentOutputArrayIndexScenario.cpp Scenarios/BufferTextureScenario.cpp Scenarios/VertexAttribBindingScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/Scenarios/TessellationDrawModeScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/TessellationDrawModeScenario.cpp new file mode 100644 index 00000000..c4c9489f --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/TessellationDrawModeScenario.cpp @@ -0,0 +1,226 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationDrawModeScenario.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - GL_PATCHES AND THE TESSELLATION PIPELINE ARE EACH OTHER'S ONLY PARTNER. +// +// GL 4.6 core 10.1 states the rule in both directions, and both are GL_INVALID_OPERATION: +// a program with a tessellation evaluation shader may only be drawn with GL_PATCHES, and +// GL_PATCHES may only be drawn with such a program. MobileGL's draw-mode validator +// implemented the geometry-shader input-primitive rule and NOTHING for tessellation, which +// is two of the four sites KHR-GL43.transform_feedback.api_errors_test checks (all four +// share one copy-pasted message string, so the trace cannot say which one it stopped at). +// +// Needs a real context: the validator returns before either rule when no backend object is +// active, so the GPU-free negative-API suite cannot reach them. + +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + const char* const kVertexSource = R"(#version 420 core +void main() +{ + gl_Position = vec4(0.0, 0.0, 0.0, 1.0); +} +)"; + + const char* const kTessControlSource = R"(#version 420 core +layout(vertices = 1) out; +void main() +{ + gl_TessLevelOuter[0] = 1.0; + gl_TessLevelOuter[1] = 1.0; + gl_TessLevelOuter[2] = 1.0; + gl_TessLevelInner[0] = 1.0; + gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position; +} +)"; + + const char* const kTessEvalSource = R"(#version 420 core +layout(triangles, equal_spacing, cw) in; +void main() +{ + gl_Position = gl_in[0].gl_Position; +} +)"; + + const char* const kFragmentSource = R"(#version 420 core +out vec4 fragColor; +void main() +{ + fragColor = vec4(0.0, 1.0, 0.0, 1.0); +} +)"; + + class TessellationDrawModeScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + if (!BackendHostsTessellation()) { + GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" + << Gl().RendererString() << "); there is no patch draw to validate"; + } + } + + void TearDown() override { + if (!Ready()) return; + glUseProgram(0); + for (const GLuint program : m_programs) { + glDeleteProgram(program); + } + m_programs.clear(); + glBindVertexArray(0); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + m_vao = 0; + } + + // The same real-backend probe IoBlockNameCollisionScenario uses: 0 on a DirectGLES + // driver without GL_EXT_tessellation_shader and on a DirectVulkan device without + // the tessellationShader feature. + static bool BackendHostsTessellation() { + GLint maxTessGenLevel = 0; + glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel); + DrainErrors(); + return maxTessGenLevel >= 1; + } + + static void DrainErrors() { + for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) { + } + } + + GLuint BuildProgram(const std::vector>& stages) { + std::vector shaders; + bool ok = true; + for (const auto& [stage, source] : stages) { + const GLuint shader = glCreateShader(stage); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + GLint compiled = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + shaders.push_back(shader); + if (!compiled) { + m_buildLog = InfoLog(shader, true); + ok = false; + break; + } + } + if (!ok) { + for (const GLuint shader : shaders) glDeleteShader(shader); + return 0; + } + + const GLuint program = glCreateProgram(); + for (const GLuint shader : shaders) glAttachShader(program, shader); + glLinkProgram(program); + GLint linked = 0; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + for (const GLuint shader : shaders) glDeleteShader(shader); + if (!linked) { + m_buildLog = InfoLog(program, false); + glDeleteProgram(program); + return 0; + } + m_programs.push_back(program); + return program; + } + + static std::string InfoLog(GLuint object, bool isShader) { + GLint length = 0; + if (isShader) { + glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length); + } else { + glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length); + } + std::vector buffer(static_cast(length) + 1, '\0'); + if (isShader) { + glGetShaderInfoLog(object, length + 1, nullptr, buffer.data()); + } else { + glGetProgramInfoLog(object, length + 1, nullptr, buffer.data()); + } + return buffer.data(); + } + + const std::string& BuildLog() const { return m_buildLog; } + + GLuint m_vao = 0; + std::vector m_programs; + std::string m_buildLog; + }; + + // A tessellation program drawn with anything but GL_PATCHES. + TEST_F(TessellationDrawModeScenario, TessellationProgramRejectsNonPatchModes) { + if (!Ready()) GTEST_SKIP(); + + const GLuint program = BuildProgram({{GL_VERTEX_SHADER, kVertexSource}, + {GL_TESS_CONTROL_SHADER, kTessControlSource}, + {GL_TESS_EVALUATION_SHADER, kTessEvalSource}, + {GL_FRAGMENT_SHADER, kFragmentSource}}); + ASSERT_NE(program, 0u) << "the tessellation program did not build: " << BuildLog(); + + glUseProgram(program); + glPatchParameteri(GL_PATCH_VERTICES, 1); + DrainErrors(); + + for (const GLenum mode : {static_cast(GL_POINTS), static_cast(GL_LINES), + static_cast(GL_TRIANGLES)}) { + glDrawArrays(mode, 0, 1); + EXPECT_EQ(glGetError(), static_cast(GL_INVALID_OPERATION)) + << "mode " << mode << " must not be accepted while tessellation is active"; + DrainErrors(); + } + + // The one mode that IS accepted still is - a rule keyed any wider would break every + // patch draw in the suite. + glDrawArrays(GL_PATCHES, 0, 1); + EXPECT_EQ(glGetError(), static_cast(GL_NO_ERROR)); + DrainErrors(); + } + + // ... and the other direction: GL_PATCHES without a tessellation evaluation stage. + TEST_F(TessellationDrawModeScenario, PatchesRejectedWithoutATessellationEvaluationStage) { + if (!Ready()) GTEST_SKIP(); + + const GLuint program = + BuildProgram({{GL_VERTEX_SHADER, kVertexSource}, {GL_FRAGMENT_SHADER, kFragmentSource}}); + ASSERT_NE(program, 0u) << "the vertex/fragment program did not build: " << BuildLog(); + + glUseProgram(program); + glPatchParameteri(GL_PATCH_VERTICES, 1); + DrainErrors(); + + glDrawArrays(GL_PATCHES, 0, 1); + EXPECT_EQ(glGetError(), static_cast(GL_INVALID_OPERATION)) + << "GL_PATCHES has no meaning without a tessellation evaluation stage"; + DrainErrors(); + + // The same program with an ordinary mode is untouched. + glDrawArrays(GL_TRIANGLES, 0, 3); + EXPECT_EQ(glGetError(), static_cast(GL_NO_ERROR)); + DrainErrors(); + } + + } // namespace +} // namespace MGITest