Files
MobileGL/MobileGL/MG_IntegrationTest/Scenarios/VertexArrayEnableDisableScenario.cpp
T

265 lines
12 KiB
C++

// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/VertexArrayEnableDisableScenario.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
//
// KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes, rebuilt.
//
// The case is small and does one unusual thing twice: it turns half of
// GL_MAX_VERTEX_ATTRIBS attribute arrays on and the other half off with
// glEnableVertexArrayAttrib / glDisableVertexArrayAttrib on a vertex array object
// that is NOT bound (it binds the default one first, on purpose), draws one point
// through a program that reads exactly the enabled half, and checks the sum those
// arrays produced. Then it swaps which half is enabled, draws again through a
// SECOND program, and checks the other sum.
//
// Both draws capture into ONE four-byte transform feedback buffer, allocated once
// with immutable storage and read back with glMapBuffer - so anything that only
// works on the first capture span through a buffer fails the second check while
// leaving the first one green.
//
// It is reassembled here rather than shortened because every one of those details
// is a candidate: the unbound-VAO enables, the two-program swap, the integer
// attributes fetched with glVertexAttribIPointer at a stride wider than one
// element, the second capture span, and the fact that the sums differ ONLY in
// which arrays contributed (a fetch that ignored the enable state, or one that
// read the wrong element, lands on a different number, not on garbage).
#include <cstdio>
#include <cstring>
#include <string>
#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 {
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
const GLuint shader = glCreateShader(type);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// Declares and sums the even (parity 0) or odd (parity 1) attributes only, with the
// locations assigned by glBindAttribLocation rather than a layout qualifier - which is
// what the CTS case does, and which makes the attribute set the program reads a link
// property rather than a source one.
GLuint BuildSumProgram(int parity, int attributeCount, std::string* log) {
std::string declarations;
std::string copies = " sum = 0;\n";
for (int i = parity; i < attributeCount; i += 2) {
declarations += "in int a_" + std::to_string(i) + ";\n";
copies += " sum += a_" + std::to_string(i) + ";\n";
}
// `flat` where the CTS case has none: an integral shader output cannot be
// interpolated, so a driver is within its rights to reject the unqualified form
// even with no matching fragment input. The capture reads the same value either
// way, and the qualifier keeps this scenario portable off llvmpipe.
const std::string vertexSource = "#version 450\n\n" + declarations +
"flat out int sum;\n\nvoid main()\n{\n" + copies + "}\n";
const std::string fragmentSource = R"(#version 450
out vec4 color;
void main()
{
color = vec4(1.0);
}
)";
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, fragmentSource, log);
if (fragmentShader == 0) {
glDeleteShader(vertexShader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
const char* varying = "sum";
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
for (int i = parity; i < attributeCount; i += 2) {
const std::string name = "a_" + std::to_string(i);
glBindAttribLocation(program, static_cast<GLuint>(i), name.c_str());
}
glLinkProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
class VertexArrayEnableDisableScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &m_attributeCount);
ASSERT_GE(m_attributeCount, 16);
std::string log;
m_even = BuildSumProgram(0, m_attributeCount, &log);
ASSERT_NE(m_even, 0u) << "even program failed to build: " << log;
m_odd = BuildSumProgram(1, m_attributeCount, &log);
ASSERT_NE(m_odd, 0u) << "odd program failed to build: " << log;
// One element per attribute, read as one vertex whose stride spans them all.
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
std::vector<GLint> reference(static_cast<std::size_t>(m_attributeCount));
for (int i = 0; i < m_attributeCount; ++i) reference[static_cast<std::size_t>(i)] = i;
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(reference.size() * sizeof(GLint)),
reference.data(), GL_STATIC_DRAW);
for (int i = 0; i < m_attributeCount; ++i) {
glVertexAttribIPointer(static_cast<GLuint>(i), 1, GL_INT,
static_cast<GLsizei>(sizeof(GLint) * m_attributeCount),
reinterpret_cast<const void*>(static_cast<std::size_t>(i) * sizeof(GLint)));
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
// Immutable storage, allocated once, read back with glMapBuffer - the capture
// buffer is never respecified between the two spans.
glGenBuffers(1, &m_xfb);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_xfb);
glBufferStorage(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(GLint), nullptr, GL_MAP_READ_BIT);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfb);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "capture buffer setup";
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
if (m_xfb != 0) glDeleteBuffers(1, &m_xfb);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_even != 0) glDeleteProgram(m_even);
if (m_odd != 0) glDeleteProgram(m_odd);
ScenarioTest::TearDown();
}
// Enables one parity's arrays and disables the other's, THROUGH THE OBJECT NAME
// while a different vertex array object is bound.
void TurnOnAttributes(int enabledParity) {
glBindVertexArray(0);
for (int i = 0; i < m_attributeCount; ++i) {
if (i % 2 == enabledParity % 2) {
glEnableVertexArrayAttrib(m_vao, static_cast<GLuint>(i));
} else {
glDisableVertexArrayAttrib(m_vao, static_cast<GLuint>(i));
}
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "attribute " << i << ", parity " << enabledParity;
}
glBindVertexArray(m_vao);
}
int ExpectedSum(int parity) const {
int sum = 0;
for (int i = parity; i < m_attributeCount; i += 2) sum += i;
return sum;
}
// One capture span, read back the way the CTS case does.
int DrawAndRead(int parity) {
glUseProgram(parity == 0 ? m_even : m_odd);
glBindVertexArray(m_vao);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
const void* mapped = glMapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, GL_READ_ONLY);
if (mapped == nullptr) {
ADD_FAILURE() << "glMapBuffer returned null for parity " << parity;
return -1;
}
GLint result = -1;
std::memcpy(&result, mapped, sizeof(result));
glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
return result;
}
GLint m_attributeCount = 16;
GLuint m_even = 0;
GLuint m_odd = 0;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_xfb = 0;
};
// The case verbatim: even half on, draw, check; odd half on, draw, check.
TEST_F(VertexArrayEnableDisableScenario, EitherHalfOfTheAttributesInTurn) {
if (!Ready()) GTEST_SKIP();
TurnOnAttributes(0);
EXPECT_EQ(DrawAndRead(0), ExpectedSum(0)) << "even attributes";
TurnOnAttributes(1);
EXPECT_EQ(DrawAndRead(1), ExpectedSum(1)) << "odd attributes";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The first span on its own, so a failure of the case above can be read as "the second
// span" rather than "the enables".
TEST_F(VertexArrayEnableDisableScenario, TheEvenHalfAlone) {
if (!Ready()) GTEST_SKIP();
TurnOnAttributes(0);
EXPECT_EQ(DrawAndRead(0), ExpectedSum(0));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// And the odd half as the FIRST span, which separates "the odd program/arrays are
// wrong" from "the second span is wrong".
TEST_F(VertexArrayEnableDisableScenario, TheOddHalfAlone) {
if (!Ready()) GTEST_SKIP();
TurnOnAttributes(1);
EXPECT_EQ(DrawAndRead(1), ExpectedSum(1));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
} // namespace
} // namespace MGITest