mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix, Test] (GLImpl, MG_IntegrationTest): enforce the tessellation draw-mode rules and waive the XFB mode match for it
This commit is contained in:
@@ -199,11 +199,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
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<GenericErrorInfo>(
|
||||||
|
"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<GenericErrorInfo>("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
|
// 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
|
// 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
|
// is the tessellation pipeline's input and reaches the geometry stage already
|
||||||
// converted, so it is not constrained here.
|
// converted, so it is not constrained here.
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
|
||||||
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||||
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
||||||
Bool compatible = false;
|
Bool compatible = false;
|
||||||
@@ -239,13 +265,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// While transform feedback is active the draw's primitive type must match
|
// 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 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 constraint moves to the shader's output primitive type instead, so
|
||||||
// the draw mode itself is unconstrained here. A paused span is exempt: it
|
// the draw mode itself is unconstrained here - and a TESSELLATION EVALUATION
|
||||||
// captures nothing, so there is nothing for the mode to be incompatible with
|
// stage relocates it exactly the same way (GL 4.6 core 13.2.2 names both):
|
||||||
// (GL 4.6 core 13.2.3).
|
// 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() &&
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||||
!MG_State::pGLContext->IsTransformFeedbackPaused() &&
|
!MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) {
|
||||||
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
|
|
||||||
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
|
|
||||||
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
||||||
Bool compatible = false;
|
Bool compatible = false;
|
||||||
switch (feedbackMode) {
|
switch (feedbackMode) {
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/SsboDeclarationFormScenario.cpp
|
Scenarios/SsboDeclarationFormScenario.cpp
|
||||||
Scenarios/Glsl420DeclarationScenario.cpp
|
Scenarios/Glsl420DeclarationScenario.cpp
|
||||||
Scenarios/IoBlockNameCollisionScenario.cpp
|
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||||
|
Scenarios/TessellationDrawModeScenario.cpp
|
||||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||||
Scenarios/BufferTextureScenario.cpp
|
Scenarios/BufferTextureScenario.cpp
|
||||||
Scenarios/VertexAttribBindingScenario.cpp
|
Scenarios/VertexAttribBindingScenario.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 <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#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<std::pair<GLenum, const char*>>& stages) {
|
||||||
|
std::vector<GLuint> 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<char> buffer(static_cast<std::size_t>(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<GLuint> 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<GLenum>(GL_POINTS), static_cast<GLenum>(GL_LINES),
|
||||||
|
static_cast<GLenum>(GL_TRIANGLES)}) {
|
||||||
|
glDrawArrays(mode, 0, 1);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(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<GLenum>(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<GLenum>(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<GLenum>(GL_NO_ERROR));
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
Reference in New Issue
Block a user