mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
299 lines
12 KiB
C++
299 lines
12 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GeometryDrawModeScenario.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 - A GEOMETRY SHADER'S INPUT PRIMITIVE CONSTRAINS THE DRAW MODE, AND
|
|
// GL_NONE IS NOT A USABLE "NO GEOMETRY SHADER" SENTINEL.
|
|
//
|
|
// GL 4.6 core 11.3.1: mode must be one of the primitive types that decomposes into the
|
|
// geometry shader's declared input primitive, or the draw is GL_INVALID_OPERATION. The
|
|
// validator asked "is there a geometry stage?" by comparing the REFLECTED INPUT PRIMITIVE
|
|
// against GL_NONE - and GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry
|
|
// shader answered "no geometry stage" and every mode sailed through. The rule was therefore
|
|
// dead for exactly the geometry shaders whose input primitive rejects the most modes.
|
|
//
|
|
// KHR-GL43.transform_feedback.api_errors_test is where it showed: it draws a points-in
|
|
// geometry program with GL_LINES through glDrawTransformFeedbackInstanced and requires
|
|
// INVALID_OPERATION. The bug is not specific to that entry point - every draw shares this
|
|
// validator - so the ordinary glDrawArrays spelling is pinned here too, and the lines-in
|
|
// program is the control that proves the rule was not simply widened.
|
|
//
|
|
// Needs a real context: the validator returns before this rule when no backend object is
|
|
// active, so the GPU-free negative-API suite cannot reach it.
|
|
|
|
#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);
|
|
}
|
|
)";
|
|
|
|
// The input primitive the CTS case uses, and the one the GL_NONE sentinel erased.
|
|
// `result` is here so the same program can be captured with transform feedback.
|
|
const char* const kPointsInGeometrySource = R"(#version 420 core
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
out float result;
|
|
void main()
|
|
{
|
|
gl_Position = gl_in[0].gl_Position;
|
|
result = 1.0;
|
|
EmitVertex();
|
|
}
|
|
)";
|
|
|
|
const char* const kLinesInGeometrySource = R"(#version 420 core
|
|
layout(lines) in;
|
|
layout(points, max_vertices = 1) out;
|
|
void main()
|
|
{
|
|
gl_Position = gl_in[0].gl_Position;
|
|
EmitVertex();
|
|
}
|
|
)";
|
|
|
|
const char* const kFragmentSource = R"(#version 420 core
|
|
out vec4 fragColor;
|
|
void main()
|
|
{
|
|
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
class GeometryDrawModeScenario : public ScenarioTest {
|
|
protected:
|
|
void SetUp() override {
|
|
ScenarioTest::SetUp();
|
|
if (!Ready()) return;
|
|
glGenVertexArrays(1, &m_vao);
|
|
glBindVertexArray(m_vao);
|
|
if (!BackendHostsGeometry()) {
|
|
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " ("
|
|
<< Gl().RendererString() << "); there is no input primitive 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_geometry_shader and on a DirectVulkan device without the
|
|
// geometryShader feature.
|
|
static bool BackendHostsGeometry() {
|
|
GLint maxGeometryOutputVertices = 0;
|
|
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
|
DrainErrors();
|
|
return maxGeometryOutputVertices >= 4;
|
|
}
|
|
|
|
static void DrainErrors() {
|
|
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
}
|
|
}
|
|
|
|
GLuint BuildProgram(const char* geometrySource, const char* capturedVarying = nullptr) {
|
|
const std::vector<std::pair<GLenum, const char*>> stages = {
|
|
{GL_VERTEX_SHADER, kVertexSource},
|
|
{GL_GEOMETRY_SHADER, geometrySource},
|
|
{GL_FRAGMENT_SHADER, kFragmentSource}};
|
|
|
|
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);
|
|
if (capturedVarying != nullptr) {
|
|
glTransformFeedbackVaryings(program, 1, &capturedVarying, GL_INTERLEAVED_ATTRIBS);
|
|
}
|
|
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;
|
|
};
|
|
|
|
// GL_POINTS is the only mode that decomposes into a points input primitive.
|
|
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsEveryOtherMode) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
|
|
const GLuint program = BuildProgram(kPointsInGeometrySource);
|
|
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
|
|
|
glUseProgram(program);
|
|
DrainErrors();
|
|
|
|
for (const GLenum mode :
|
|
{static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
|
static_cast<GLenum>(GL_TRIANGLES), static_cast<GLenum>(GL_TRIANGLE_STRIP)}) {
|
|
glDrawArrays(mode, 0, 3);
|
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
<< "mode " << mode << " does not decompose into the geometry shader's points input";
|
|
DrainErrors();
|
|
}
|
|
|
|
// The one mode that IS compatible still draws.
|
|
glDrawArrays(GL_POINTS, 0, 1);
|
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
DrainErrors();
|
|
}
|
|
|
|
// The same rule reached through glDrawTransformFeedback*, which is the spelling the CTS
|
|
// case asks about. The capture span is really completed first, so GL_POINTS comes back
|
|
// GL_NO_ERROR: without that the draw would report INVALID_OPERATION for the
|
|
// never-ended-a-span reason instead and the case could not tell the two apart.
|
|
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsNonPointModesOnFeedbackDraws) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
|
|
const GLuint program = BuildProgram(kPointsInGeometrySource, "result");
|
|
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
|
|
|
GLuint feedback = 0;
|
|
glGenTransformFeedbacks(1, &feedback);
|
|
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback);
|
|
GLuint captureBuffer = 0;
|
|
glGenBuffers(1, &captureBuffer);
|
|
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, captureBuffer);
|
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64, nullptr, GL_STATIC_DRAW);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
|
glUseProgram(program);
|
|
DrainErrors();
|
|
|
|
glBeginTransformFeedback(GL_POINTS);
|
|
glDrawArrays(GL_POINTS, 0, 1);
|
|
glEndTransformFeedback();
|
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the capture span did not complete";
|
|
|
|
glDrawTransformFeedbackInstanced(GL_LINES, feedback, 1);
|
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
<< "glDrawTransformFeedbackInstanced must honour the geometry input primitive";
|
|
DrainErrors();
|
|
|
|
glDrawTransformFeedbackStreamInstanced(GL_LINES, feedback, 0, 1);
|
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
<< "glDrawTransformFeedbackStreamInstanced must honour the geometry input primitive";
|
|
DrainErrors();
|
|
|
|
// The compatible mode replays the captured span with no error at all, which is what
|
|
// makes the two assertions above about the MODE and not about the span.
|
|
glDrawTransformFeedbackInstanced(GL_POINTS, feedback, 1);
|
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
|
<< "a compatible mode must still replay the captured span";
|
|
DrainErrors();
|
|
|
|
glUseProgram(0);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
|
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
|
glDeleteBuffers(1, &captureBuffer);
|
|
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
|
|
glDeleteTransformFeedbacks(1, &feedback);
|
|
DrainErrors();
|
|
}
|
|
|
|
// The control: a lines-in geometry shader is a NON-zero input primitive, so it exercised
|
|
// the rule even before the fix. It must still accept the line modes and still reject the
|
|
// others - a fix that widened the rule instead of repairing its guard breaks this.
|
|
TEST_F(GeometryDrawModeScenario, LinesInGeometryProgramStillAcceptsLineModesOnly) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
|
|
const GLuint program = BuildProgram(kLinesInGeometrySource);
|
|
ASSERT_NE(program, 0u) << "the lines-in geometry program did not build: " << BuildLog();
|
|
|
|
glUseProgram(program);
|
|
DrainErrors();
|
|
|
|
for (const GLenum mode : {static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
|
static_cast<GLenum>(GL_LINE_LOOP)}) {
|
|
glDrawArrays(mode, 0, 2);
|
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
|
<< "mode " << mode << " decomposes into lines and must be accepted";
|
|
DrainErrors();
|
|
}
|
|
|
|
for (const GLenum mode : {static_cast<GLenum>(GL_POINTS), static_cast<GLenum>(GL_TRIANGLES)}) {
|
|
glDrawArrays(mode, 0, 3);
|
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
<< "mode " << mode << " does not decompose into lines";
|
|
DrainErrors();
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace MGITest
|