mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
265 lines
12 KiB
C++
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
|