Files
MobileGL/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp
T

1286 lines
49 KiB
C++

// MobileGL - MobileGL/MG_Test/VertexArray/VertexArrayTest.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
#include <gtest/gtest.h>
#include <limits>
#include "Includes.h"
#include "Init.h"
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/GLState/Core.h>
using namespace MobileGL;
class VertexArrayTest : public ::testing::Test {
protected:
SharedPtr<MG_State::GLState::BufferObject> CreateTestVBO() {
Vector<Uint> bufferNames;
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
auto vbo = MobileGL::MG_State::pGLContext->CreateBufferObject(bufferNames[0]);
MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).Bind(vbo);
Vector<float> vertexData = {0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f};
SizeT byteSize = vertexData.size() * sizeof(float);
vbo->Resize(byteSize);
DataPtr ptr{.data = vertexData.data(), .size = byteSize};
vbo->UploadData(ptr, 0);
return vbo;
}
// GL error flags are sticky per error code and the context outlives an individual test in this
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
static void DrainPendingGlErrors() {
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
}
}
// The call under test must raise exactly the expected error and nothing more: a second pending
// error means one entry point queued several, which GetError() would hand out at an unrelated
// call site later on.
static void ExpectSingleGlError(GLenum expected) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
}
void SetUp() override {
MobileGL::Initialize();
DrainPendingGlErrors();
}
void TearDown() override {
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
}
};
TEST_F(VertexArrayTest, GenerateAndBindVAO) {
Vector<Uint> vaoNames;
MobileGL::MG_State::pGLContext->GenVertexArrayNames(2, vaoNames);
auto vao0 = MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[0]);
auto vao1 = MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[1]);
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[0]);
ASSERT_EQ(MobileGL::MG_State::pGLContext->GetBoundVertexArray(), vao0);
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[1]);
ASSERT_EQ(MobileGL::MG_State::pGLContext->GetBoundVertexArray(), vao1);
// MobileGL::MG_State::pGLContext->BindVertexArray(0);
// ASSERT_EQ(MobileGL::MG_State::pGLContext->GetBoundVertexArray(), nullptr);
// Do not detect if it supports default VAO
}
// GL 3.3 core 2.10 name lifecycle - the same three rules the other object families assert:
// deleting an unknown name is silent, a released reservation is recycled, and binding a dead
// name is INVALID_OPERATION.
TEST_F(VertexArrayTest, DeleteOfUnknownOrAlreadyDeletedVertexArrayNameIsSilent) {
GLuint vao = 0;
MG_Impl::GLImpl::GenVertexArrays(1, &vao);
ASSERT_NE(vao, 0u);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Not a small literal: other tests in this binary share the context and generate names in
// bulk, so a low number may well be a legitimately reserved name here.
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
MG_Impl::GLImpl::DeleteVertexArrays(2, unknownNames);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(VertexArrayTest, DeleteGeneratedButUnboundVertexArrayNameReleasesReservationAndBindFails) {
GLuint vao = 0;
MG_Impl::GLImpl::GenVertexArrays(1, &vao);
ASSERT_NE(vao, 0u);
ASSERT_TRUE(MG_State::pGLContext->ValidateVertexArrayName(vao));
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_FALSE(MG_State::pGLContext->ValidateVertexArrayName(vao));
MG_Impl::GLImpl::BindVertexArray(vao);
ExpectSingleGlError(GL_INVALID_OPERATION);
GLuint recycled = 0;
MG_Impl::GLImpl::GenVertexArrays(1, &recycled);
EXPECT_EQ(recycled, vao);
}
TEST_F(VertexArrayTest, VertexAttributeSetup) {
Vector<Uint> vaoNames;
MobileGL::MG_State::pGLContext->GenVertexArrayNames(1, vaoNames);
auto vao = MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[0]);
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[0]);
auto vbo = CreateTestVBO();
vao->EnableAttribute(0);
vao->SetAttributeFormat(0, 4, DataType::Float32, false, 8 * sizeof(float), 0, false);
vao->BindAttributeBuffer(0, vbo);
vao->EnableAttribute(1);
vao->SetAttributeFormat(1, 4, DataType::Float32, false, 8 * sizeof(float), 4 * sizeof(float), false);
vao->BindAttributeBuffer(1, vbo);
const auto& attr0 = vao->GetAttribute(0);
ASSERT_TRUE(attr0.Enabled);
ASSERT_EQ(attr0.Size, 4);
ASSERT_EQ(attr0.Type, DataType::Float32);
ASSERT_EQ(attr0.Stride, 8 * sizeof(float));
ASSERT_EQ(attr0.Offset, 0);
ASSERT_EQ(attr0.Buffer, vbo);
const auto& attr1 = vao->GetAttribute(1);
ASSERT_TRUE(attr1.Enabled);
ASSERT_EQ(attr1.Offset, 4 * sizeof(float));
vao->DisableAttribute(1);
ASSERT_FALSE(vao->IsAttributeEnabled(1));
}
TEST_F(VertexArrayTest, IndexBufferBinding) {
Vector<Uint> vaoNames;
MobileGL::MG_State::pGLContext->GenVertexArrayNames(1, vaoNames);
auto vao = MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[0]);
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[0]);
Vector<Uint> bufferNames;
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
auto ebo = MobileGL::MG_State::pGLContext->CreateBufferObject(bufferNames[0]);
MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).Bind(ebo);
Vector<Uint> indices = {0, 1, 2};
SizeT byteSize = indices.size() * sizeof(Uint);
ebo->Resize(byteSize);
DataPtr ptr{.data = indices.data(), .size = byteSize};
ebo->UploadData(ptr, 0);
MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).Bind(ebo);
ASSERT_EQ(MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).GetBoundObject(), ebo);
Vector<Uint> newEboNames;
MobileGL::MG_State::pGLContext->GenBufferNames(1, newEboNames);
auto newEbo = MobileGL::MG_State::pGLContext->CreateBufferObject(newEboNames[0]);
MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).Bind(newEbo);
ASSERT_EQ(MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).GetBoundObject(), newEbo);
}
TEST_F(VertexArrayTest, DeleteVAO) {
Vector<Uint> vaoNames;
MobileGL::MG_State::pGLContext->GenVertexArrayNames(1, vaoNames);
auto vao = MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[0]);
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[0]);
ASSERT_EQ(MobileGL::MG_State::pGLContext->GetBoundVertexArray(), vao);
MobileGL::MG_State::pGLContext->MarkVertexArrayForDeletion(vaoNames[0]);
ASSERT_FALSE(MobileGL::MG_State::pGLContext->ValidateVertexArrayObject(vaoNames[0]));
ASSERT_EQ(MobileGL::MG_State::pGLContext->GetVertexArrayObject(vaoNames[0]), nullptr);
const auto boundVao = MobileGL::MG_State::pGLContext->GetBoundVertexArray();
ASSERT_NE(boundVao, nullptr);
ASSERT_EQ(boundVao->GetExternalIndex(), 0u);
}
TEST_F(VertexArrayTest, ValidateNamesAndObjects) {
const Uint count = 5;
Vector<Uint> vaoNames;
MobileGL::MG_State::pGLContext->GenVertexArrayNames(count, vaoNames);
for (Uint i = 0; i < count; i++) {
ASSERT_TRUE(MobileGL::MG_State::pGLContext->ValidateVertexArrayName(vaoNames[i]));
ASSERT_FALSE(MobileGL::MG_State::pGLContext->ValidateVertexArrayObject(vaoNames[i]));
}
for (Uint i = 0; i < count; i += 2) {
MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[i]);
ASSERT_TRUE(MobileGL::MG_State::pGLContext->ValidateVertexArrayObject(vaoNames[i]));
}
for (Uint i = 1; i < count; i += 2) {
MobileGL::MG_State::pGLContext->MarkVertexArrayForDeletion(vaoNames[i]);
ASSERT_FALSE(MobileGL::MG_State::pGLContext->ValidateVertexArrayName(vaoNames[i]));
}
}
TEST_F(VertexArrayTest, MultipleAttributes) {
Vector<Uint> vaoNames;
MobileGL::MG_State::pGLContext->GenVertexArrayNames(1, vaoNames);
auto vao = MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[0]);
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[0]);
auto vboPos = CreateTestVBO();
Vector<Uint> vboNormalNames;
MobileGL::MG_State::pGLContext->GenBufferNames(1, vboNormalNames);
auto vboNormal = MobileGL::MG_State::pGLContext->CreateBufferObject(vboNormalNames[0]);
Vector<float> normals(12, 0.5f);
SizeT byteSize = normals.size() * sizeof(float);
vboNormal->Resize(byteSize);
DataPtr ptr{.data = normals.data(), .size = byteSize};
vboNormal->UploadData(ptr, 0);
vao->EnableAttribute(0);
vao->SetAttributeFormat(0, 3, DataType::Float32, false, 3 * sizeof(float), 0, false);
vao->BindAttributeBuffer(0, vboPos);
vao->EnableAttribute(1);
vao->SetAttributeFormat(1, 3, DataType::Float32, true, 3 * sizeof(float), 0, false);
vao->BindAttributeBuffer(1, vboNormal);
vao->EnableAttribute(2);
vao->SetAttributeFormat(2, 4, DataType::Uint8, true, 4 * sizeof(Uint8), 0, false);
const auto& attr0 = vao->GetAttribute(0);
ASSERT_EQ(attr0.Size, 3);
ASSERT_EQ(attr0.Buffer, vboPos);
const auto& attr1 = vao->GetAttribute(1);
ASSERT_TRUE(attr1.Normalized);
ASSERT_EQ(attr1.Buffer, vboNormal);
const auto& attr2 = vao->GetAttribute(2);
ASSERT_EQ(attr2.Type, DataType::Uint8);
ASSERT_EQ(attr2.Buffer, nullptr);
vao->DisableAttribute(1);
vao->EnableAttribute(1);
ASSERT_TRUE(vao->IsAttributeEnabled(1));
}
TEST_F(VertexArrayTest, BoundVAOPreservesState) {
Vector<Uint> vaoNames;
MobileGL::MG_State::pGLContext->GenVertexArrayNames(2, vaoNames);
auto vao1 = MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[0]);
auto vao2 = MobileGL::MG_State::pGLContext->CreateVertexArrayObject(vaoNames[1]);
auto vbo = CreateTestVBO();
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[0]);
vao1->EnableAttribute(0);
vao1->SetAttributeFormat(0, 4, DataType::Float32, false, 0, 0, false);
vao1->BindAttributeBuffer(0, vbo);
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[1]);
vao2->EnableAttribute(1);
vao2->SetAttributeFormat(1, 3, DataType::Float32, true, 0, 0, false);
MobileGL::MG_State::pGLContext->BindVertexArray(vaoNames[0]);
const auto& attr = vao1->GetAttribute(0);
ASSERT_TRUE(attr.Enabled);
ASSERT_EQ(attr.Size, 4);
ASSERT_EQ(attr.Buffer, vbo);
ASSERT_FALSE(vao2->IsAttributeEnabled(0));
}
// The current-value array must cover the full attribute capacity. It used to be sized 16 while the
// DirectVulkan draw path indexed it with shader locations up to 31, reading past the end; the only
// guard was MOBILEGL_ASSERT, which expands to nothing outside debug builds.
TEST_F(VertexArrayTest, CurrentVertexAttributeStorageCoversFullCapacity) {
constexpr Uint capacity = static_cast<Uint>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
const Uint highIndex = capacity - 1;
MG_State::pGLContext->SetCurrentVertexAttributeFloat(highIndex, {1.0f, 2.0f, 3.0f, 4.0f});
const auto& stored = MG_State::pGLContext->GetCurrentVertexAttribute(highIndex);
EXPECT_FLOAT_EQ(stored.floatValue[0], 1.0f);
EXPECT_FLOAT_EQ(stored.floatValue[3], 4.0f);
// Neighbouring slots keep the GL default of (0, 0, 0, 1).
const auto& untouched = MG_State::pGLContext->GetCurrentVertexAttribute(highIndex - 1);
EXPECT_FLOAT_EQ(untouched.floatValue[0], 0.0f);
EXPECT_FLOAT_EQ(untouched.floatValue[3], 1.0f);
// Out-of-range access must be bounded at runtime, not just asserted in debug builds.
const auto& outOfRange = MG_State::pGLContext->GetCurrentVertexAttribute(capacity);
EXPECT_FLOAT_EQ(outOfRange.floatValue[0], 0.0f);
EXPECT_FLOAT_EQ(outOfRange.floatValue[3], 1.0f);
MG_State::pGLContext->SetCurrentVertexAttributeFloat(capacity, {9.0f, 9.0f, 9.0f, 9.0f});
EXPECT_FLOAT_EQ(MG_State::pGLContext->GetCurrentVertexAttribute(highIndex).floatValue[0], 1.0f);
}
// A binding point the backend cannot address as an attribute must be rejected: the default mapping
// is the identity, so accepting it would resolve into an attribute index the backend then rejects on
// every draw.
TEST_F(VertexArrayTest, VertexBindingIndexIsBoundedByTheAdvertisedAttribLimit) {
Vector<Uint> vaoNames;
MG_State::pGLContext->GenVertexArrayNames(1, vaoNames);
MG_State::pGLContext->CreateVertexArrayObject(vaoNames[0]);
MG_State::pGLContext->BindVertexArray(vaoNames[0]);
MG_State::pGLContext->ClearErrors();
const GLuint outOfRange = MG_Impl::GLImpl::VertexArrayImpl::GetMaxVertexAttribs();
MG_Impl::GLImpl::VertexAttribBinding(0, outOfRange);
// Asserting the exact code (rather than just "some error") also consumes it, so the next test
// does not inherit it - GL error flags are sticky and this context is shared.
ExpectSingleGlError(GL_INVALID_VALUE);
}
// The default attribute -> binding-point mapping is the identity. It used to be a 16-element literal
// list, so every attribute at or above 16 silently resolved against binding point 0 instead.
TEST_F(VertexArrayTest, DefaultAttributeBindingIsIdentityAcrossFullCapacity) {
Vector<Uint> vaoNames;
MG_State::pGLContext->GenVertexArrayNames(1, vaoNames);
auto vao = MG_State::pGLContext->CreateVertexArrayObject(vaoNames[0]);
MG_State::pGLContext->BindVertexArray(vaoNames[0]);
auto vbo = CreateTestVBO();
constexpr Uint kHighAttrib = 20;
static_assert(kHighAttrib >= 16, "must exceed the old 16-entry identity list");
static_assert(kHighAttrib < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
static_assert(kHighAttrib < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIB_BINDINGS);
// Binding point kHighAttrib must feed attribute kHighAttrib with no explicit SetAttributeBinding.
vao->SetAttributeFormatSeparate(kHighAttrib, 3, DataType::Float32, false, false, 12);
vao->SetBindingBuffer(kHighAttrib, vbo, 16, 24);
const auto& attr = vao->GetAttribute(kHighAttrib);
EXPECT_EQ(attr.Buffer, vbo);
EXPECT_EQ(attr.Stride, 24);
EXPECT_EQ(attr.Offset, 28u); // binding offset (16) + attribute relative offset (12)
// Attribute 0 must not have been dragged along by binding point kHighAttrib.
EXPECT_EQ(vao->GetAttribute(0).Buffer, nullptr);
}
using namespace MobileGL::MG_Impl::GLImpl;
class GeneralVertexArrayTest : public ::testing::Test {
protected:
void SetUp() override { MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>(); }
void TearDown() override {
}
GLuint CreateVAO() {
GLuint vao;
GenVertexArrays(1, &vao);
BindVertexArray(vao);
return vao;
}
GLuint CreateVBO(GLenum target, GLsizeiptr size, const void* data = nullptr) {
GLuint vbo;
GenBuffers(1, &vbo);
BindBuffer(target, vbo);
BufferData(target, size, data, GL_STATIC_DRAW);
return vbo;
}
};
TEST_F(GeneralVertexArrayTest, General_VAOLifecycle) {
GLuint vaos[3];
GenVertexArrays(3, vaos);
EXPECT_NE(vaos[0], 0);
EXPECT_NE(vaos[1], 0);
EXPECT_NE(vaos[2], 0);
EXPECT_NE(vaos[0], vaos[1]);
BindVertexArray(vaos[0]);
EXPECT_EQ(IsVertexArray(vaos[0]), GL_TRUE);
GLuint deleteVao = vaos[1];
DeleteVertexArrays(1, &deleteVao);
BindVertexArray(deleteVao);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
DeleteVertexArrays(1, &vaos[0]);
DeleteVertexArrays(1, &vaos[2]);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_CreateVertexArraysCreatesObjectsWithoutChangingBinding) {
GLuint bound = CreateVAO();
auto boundObj = MG_State::pGLContext->GetBoundVertexArray();
ASSERT_NE(boundObj, nullptr);
GLuint vaos[2] = {};
CreateVertexArrays(2, vaos);
EXPECT_NE(vaos[0], 0u);
EXPECT_NE(vaos[1], 0u);
EXPECT_NE(vaos[0], vaos[1]);
EXPECT_EQ(IsVertexArray(vaos[0]), GL_TRUE);
EXPECT_EQ(IsVertexArray(vaos[1]), GL_TRUE);
EXPECT_EQ(MG_State::pGLContext->GetBoundVertexArray(), boundObj);
DeleteVertexArrays(2, vaos);
DeleteVertexArrays(1, &bound);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_CreateVertexArraysRejectsNegativeCount) {
GLuint vao = 0;
CreateVertexArrays(-1, &vao);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
}
TEST_F(GeneralVertexArrayTest, General_DirectStateAccessConfiguresNamedVAOWithoutChangingBinding) {
GLuint bound = CreateVAO();
auto boundObj = MG_State::pGLContext->GetBoundVertexArray();
ASSERT_NE(boundObj, nullptr);
GLuint vao = 0;
CreateVertexArrays(1, &vao);
GLuint vertexBuffer = 0;
GLuint indexBuffer = 0;
CreateBuffers(1, &vertexBuffer);
CreateBuffers(1, &indexBuffer);
NamedBufferData(vertexBuffer, 256, nullptr, GL_STATIC_DRAW);
NamedBufferData(indexBuffer, 128, nullptr, GL_STATIC_DRAW);
VertexArrayVertexBuffer(vao, 2, vertexBuffer, 16, 24);
VertexArrayAttribFormat(vao, 2, 3, GL_FLOAT, GL_TRUE, 12);
EnableVertexArrayAttrib(vao, 2);
VertexArrayElementBuffer(vao, indexBuffer);
auto vaoObj = MG_State::pGLContext->GetVertexArrayObject(vao);
ASSERT_NE(vaoObj, nullptr);
const auto& attr = vaoObj->GetAttribute(2);
EXPECT_TRUE(attr.Enabled);
EXPECT_EQ(attr.Size, 3);
EXPECT_EQ(attr.Type, DataType::Float32);
EXPECT_TRUE(attr.Normalized);
EXPECT_FALSE(attr.IsInteger);
EXPECT_EQ(attr.Stride, 24);
// The flat attribute view holds the resolved effective offset:
// binding offset (16) + attribute relative offset (12).
EXPECT_EQ(attr.Offset, 28);
EXPECT_EQ(attr.Buffer, MG_State::pGLContext->GetBufferObject(vertexBuffer));
EXPECT_EQ(vaoObj->GetIndexBufferBindingSlot().GetBoundObject(), MG_State::pGLContext->GetBufferObject(indexBuffer));
EXPECT_EQ(MG_State::pGLContext->GetBoundVertexArray(), boundObj);
DeleteVertexArrays(1, &vao);
DeleteVertexArrays(1, &bound);
GLuint buffers[] = {vertexBuffer, indexBuffer};
DeleteBuffers(2, buffers);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_DirectStateAccessIntegerAttribAndUnbindElementBuffer) {
GLuint vao = 0;
GLuint indexBuffer = 0;
CreateVertexArrays(1, &vao);
CreateBuffers(1, &indexBuffer);
VertexArrayAttribIFormat(vao, 1, 4, GL_UNSIGNED_INT, 8);
EnableVertexArrayAttrib(vao, 1);
VertexArrayElementBuffer(vao, indexBuffer);
VertexArrayElementBuffer(vao, 0);
auto vaoObj = MG_State::pGLContext->GetVertexArrayObject(vao);
ASSERT_NE(vaoObj, nullptr);
const auto& attr = vaoObj->GetAttribute(1);
EXPECT_TRUE(attr.Enabled);
EXPECT_EQ(attr.Size, 4);
EXPECT_EQ(attr.Type, DataType::Uint32);
EXPECT_TRUE(attr.IsInteger);
EXPECT_FALSE(attr.Normalized);
EXPECT_EQ(attr.Offset, 8);
EXPECT_EQ(vaoObj->GetIndexBufferBindingSlot().GetBoundObject(), nullptr);
DeleteVertexArrays(1, &vao);
DeleteBuffers(1, &indexBuffer);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_VertexAttributeConfiguration) {
GLuint vao = CreateVAO();
GLuint vbo = CreateVBO(GL_ARRAY_BUFFER, 128);
VertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
EnableVertexAttribArray(0);
VertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
EnableVertexAttribArray(1);
auto vaoObj = MG_State::pGLContext->GetVertexArrayObject(vao);
ASSERT_NE(vaoObj, nullptr);
const auto& attr0 = vaoObj->GetAttribute(0);
EXPECT_TRUE(attr0.Enabled);
EXPECT_EQ(attr0.Size, 3);
EXPECT_EQ(attr0.Type, DataType::Float32);
EXPECT_EQ(attr0.Offset, 0);
const auto& attr1 = vaoObj->GetAttribute(1);
EXPECT_TRUE(attr1.Enabled);
EXPECT_EQ(attr1.Offset, 3 * sizeof(float));
DisableVertexAttribArray(1);
EXPECT_FALSE(vaoObj->GetAttribute(1).Enabled);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_IndexBufferBinding) {
GLuint vao = CreateVAO();
GLuint ebo = CreateVBO(GL_ELEMENT_ARRAY_BUFFER, 256);
auto vaoObj = MG_State::pGLContext->GetVertexArrayObject(vao);
ASSERT_NE(vaoObj, nullptr);
GLuint newEbo;
GenBuffers(1, &newEbo);
BindBuffer(GL_ELEMENT_ARRAY_BUFFER, newEbo);
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).GetBoundObject(),
MG_State::pGLContext->GetBufferObject(newEbo));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_ElementArrayBufferBindingIsVaoLocalAndZeroUnbinds) {
GLuint vao1 = CreateVAO();
GLuint ebo1;
GenBuffers(1, &ebo1);
BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo1);
GLint binding = -1;
GetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &binding);
EXPECT_EQ(binding, static_cast<GLint>(ebo1));
auto vaoObj1 = MG_State::pGLContext->GetVertexArrayObject(vao1);
ASSERT_NE(vaoObj1, nullptr);
EXPECT_EQ(vaoObj1->GetIndexBufferBindingSlot().GetBoundObject(), MG_State::pGLContext->GetBufferObject(ebo1));
GLuint vao2 = CreateVAO();
auto vaoObj2 = MG_State::pGLContext->GetVertexArrayObject(vao2);
ASSERT_NE(vaoObj2, nullptr);
GetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &binding);
EXPECT_EQ(binding, 0);
EXPECT_EQ(vaoObj2->GetIndexBufferBindingSlot().GetBoundObject(), nullptr);
BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(MG_State::pGLContext->GetBufferObject(0), nullptr);
EXPECT_EQ(IsBuffer(0), GL_FALSE);
BindVertexArray(vao1);
GetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &binding);
EXPECT_EQ(binding, static_cast<GLint>(ebo1));
BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
GetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &binding);
EXPECT_EQ(binding, 0);
EXPECT_EQ(vaoObj1->GetIndexBufferBindingSlot().GetBoundObject(), nullptr);
EXPECT_EQ(MG_State::pGLContext->GetBufferObject(0), nullptr);
EXPECT_EQ(IsBuffer(0), GL_FALSE);
DeleteVertexArrays(1, &vao1);
DeleteVertexArrays(1, &vao2);
DeleteBuffers(1, &ebo1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_ClientSideVertexAttribPointerIsAccepted) {
GLuint vao = CreateVAO();
BindBuffer(GL_ARRAY_BUFFER, 0);
float vertices[] = {0.0f, 0.0f, 1.0f, 1.0f};
VertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);
EnableVertexAttribArray(0);
auto vaoObj = MG_State::pGLContext->GetVertexArrayObject(vao);
ASSERT_NE(vaoObj, nullptr);
const auto& attr = vaoObj->GetAttribute(0);
EXPECT_TRUE(attr.Enabled);
EXPECT_EQ(attr.Buffer, nullptr);
EXPECT_EQ(attr.Offset, reinterpret_cast<SizeT>(vertices));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_IntegerAttributes) {
GLuint vao = CreateVAO();
GLuint vbo = CreateVBO(GL_ARRAY_BUFFER, 128);
VertexAttribIPointer(2, 4, GL_UNSIGNED_INT, sizeof(GLuint) * 8, (void*)(sizeof(GLuint) * 4));
EnableVertexAttribArray(2);
auto vaoObj = MG_State::pGLContext->GetVertexArrayObject(vao);
const auto& attr = vaoObj->GetAttribute(2);
EXPECT_TRUE(attr.Enabled);
EXPECT_EQ(attr.Size, 4);
EXPECT_EQ(attr.Type, DataType::Uint32);
EXPECT_EQ(attr.Offset, sizeof(GLuint) * 4);
EXPECT_TRUE(attr.IsInteger);
EXPECT_FALSE(attr.Normalized);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_StatePreservation) {
GLuint vao1 = CreateVAO();
GLuint vbo1 = CreateVBO(GL_ARRAY_BUFFER, 64);
VertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
EnableVertexAttribArray(0);
GLuint vao2;
GenVertexArrays(1, &vao2);
BindVertexArray(vao2);
GLuint vbo2 = CreateVBO(GL_ARRAY_BUFFER, 128);
VertexAttribPointer(1, 3, GL_FLOAT, GL_TRUE, 0, nullptr);
EnableVertexAttribArray(1);
BindVertexArray(vao1);
auto vaoObj1 = MG_State::pGLContext->GetVertexArrayObject(vao1);
EXPECT_TRUE(vaoObj1->IsAttributeEnabled(0));
EXPECT_FALSE(vaoObj1->IsAttributeEnabled(1));
auto vaoObj2 = MG_State::pGLContext->GetVertexArrayObject(vao2);
EXPECT_TRUE(vaoObj2->IsAttributeEnabled(1));
EXPECT_FALSE(vaoObj2->IsAttributeEnabled(0));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_ErrorConditions) {
ASSERT_NE(MG_State::pGLContext->GetBoundVertexArray(), nullptr);
BindBuffer(GL_ARRAY_BUFFER, 0);
VertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLuint vao = CreateVAO();
EnableVertexAttribArray(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
BindBuffer(GL_ARRAY_BUFFER, 0);
VertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLuint vbo = CreateVBO(GL_ARRAY_BUFFER, 64);
VertexAttribPointer(0, 3, 0xFFFFFFFF, GL_FALSE, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
VertexAttribPointer(0, 5, GL_FLOAT, GL_FALSE, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_ComplexUsage) {
GLuint vao1 = CreateVAO();
GLuint vboPos = CreateVBO(GL_ARRAY_BUFFER, 256);
GLuint vboColor = CreateVBO(GL_ARRAY_BUFFER, 128);
GLuint ebo1 = CreateVBO(GL_ELEMENT_ARRAY_BUFFER, 64);
VertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
EnableVertexAttribArray(0);
BindBuffer(GL_ARRAY_BUFFER, vboColor);
VertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, nullptr);
EnableVertexAttribArray(1);
GLuint vao2;
GenVertexArrays(1, &vao2);
BindVertexArray(vao2);
GLuint vboNormal = CreateVBO(GL_ARRAY_BUFFER, 192);
GLuint ebo2 = CreateVBO(GL_ELEMENT_ARRAY_BUFFER, 96);
VertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
EnableVertexAttribArray(2);
BindVertexArray(vao1);
auto vaoObj1 = MG_State::pGLContext->GetVertexArrayObject(vao1);
EXPECT_TRUE(vaoObj1->IsAttributeEnabled(0));
EXPECT_TRUE(vaoObj1->IsAttributeEnabled(1));
EXPECT_FALSE(vaoObj1->IsAttributeEnabled(2));
EXPECT_NE(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).GetBoundObject(), nullptr);
BindVertexArray(vao2);
auto vaoObj2 = MG_State::pGLContext->GetVertexArrayObject(vao2);
EXPECT_TRUE(vaoObj2->IsAttributeEnabled(2));
EXPECT_FALSE(vaoObj2->IsAttributeEnabled(0));
EXPECT_NE(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).GetBoundObject(), nullptr);
DeleteVertexArrays(1, &vao1);
DeleteVertexArrays(1, &vao2);
GLuint buffers[] = {vboPos, vboColor, ebo1, vboNormal, ebo2};
DeleteBuffers(5, buffers);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_DeleteBoundVAO) {
GLuint vao = CreateVAO();
GLuint vbo = CreateVBO(GL_ARRAY_BUFFER, 64);
VertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
EnableVertexAttribArray(0);
DeleteVertexArrays(1, &vao);
const auto boundVao = MG_State::pGLContext->GetBoundVertexArray();
ASSERT_NE(boundVao, nullptr);
EXPECT_EQ(boundVao->GetExternalIndex(), 0u);
EXPECT_EQ(MG_State::pGLContext->GetVertexArrayObject(vao), nullptr);
VertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(GeneralVertexArrayTest, General_ElementBufferBindingPoint) {
GLuint vao1, vao2;
GenVertexArrays(1, &vao1);
GenVertexArrays(1, &vao2);
GLuint ebo1, ebo2;
GenBuffers(1, &ebo1);
GenBuffers(1, &ebo2);
BindVertexArray(vao1);
BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo1);
BindVertexArray(vao2);
BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo2);
auto vaoObj1 = MG_State::pGLContext->GetVertexArrayObject(vao1);
auto vaoObj2 = MG_State::pGLContext->GetVertexArrayObject(vao2);
EXPECT_EQ(vaoObj1->GetIndexBufferBindingSlot().GetBoundObject(), MG_State::pGLContext->GetBufferObject(ebo1));
EXPECT_EQ(vaoObj2->GetIndexBufferBindingSlot().GetBoundObject(), MG_State::pGLContext->GetBufferObject(ebo2));
BindVertexArray(vao1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo2);
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).GetBoundObject(),
MG_State::pGLContext->GetBufferObject(ebo2));
BindVertexArray(vao2);
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).GetBoundObject(),
MG_State::pGLContext->GetBufferObject(ebo2));
BindVertexArray(0);
// BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo1);
// EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
// Do not detect if it supports default VAO
BindVertexArray(vao1);
DeleteVertexArrays(1, &vao1);
BindVertexArray(vao1);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(MG_State::pGLContext->GetVertexArrayObject(vao1).get(), nullptr);
BindVertexArray(vao2);
DeleteBuffers(1, &ebo2);
EXPECT_EQ(vaoObj2->GetIndexBufferBindingSlot().GetBoundObject().get(), nullptr);
GLuint ebo3;
GenBuffers(1, &ebo3);
BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo3);
EXPECT_EQ(vaoObj2->GetIndexBufferBindingSlot().GetBoundObject(), MG_State::pGLContext->GetBufferObject(ebo3));
DeleteVertexArrays(1, &vao2);
DeleteBuffers(1, &ebo1);
DeleteBuffers(1, &ebo3);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// GL_CURRENT_VERTEX_ATTRIB is per-context state that exists for every index below
// GL_MAX_VERTEX_ATTRIBS, and defaults to (0, 0, 0, 1).
TEST_F(GeneralVertexArrayTest, General_CurrentVertexAttribRoundTripsAtHighestLegalIndex) {
CreateVAO();
const GLuint highIndex = VertexArrayImpl::GetMaxVertexAttribs() - 1;
ASSERT_GT(highIndex, 0u);
VertexAttrib4f(highIndex, 1.0f, 2.0f, 3.0f, 4.0f);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLfloat values[4] = {-1.0f, -1.0f, -1.0f, -1.0f};
GetVertexAttribfv(highIndex, GL_CURRENT_VERTEX_ATTRIB, values);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FLOAT_EQ(values[0], 1.0f);
EXPECT_FLOAT_EQ(values[1], 2.0f);
EXPECT_FLOAT_EQ(values[2], 3.0f);
EXPECT_FLOAT_EQ(values[3], 4.0f);
// Untouched attributes keep the GL default of (0, 0, 0, 1).
GLfloat defaults[4] = {-1.0f, -1.0f, -1.0f, -1.0f};
GetVertexAttribfv(highIndex - 1, GL_CURRENT_VERTEX_ATTRIB, defaults);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FLOAT_EQ(defaults[0], 0.0f);
EXPECT_FLOAT_EQ(defaults[1], 0.0f);
EXPECT_FLOAT_EQ(defaults[2], 0.0f);
EXPECT_FLOAT_EQ(defaults[3], 1.0f);
}
// glVertexAttrib{1,2,3}f fill the components the caller omitted with (0, 0, 1).
TEST_F(GeneralVertexArrayTest, General_CurrentVertexAttribFillsOmittedComponents) {
CreateVAO();
VertexAttrib1f(1, 7.0f);
GLfloat one[4] = {};
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, one);
EXPECT_FLOAT_EQ(one[0], 7.0f);
EXPECT_FLOAT_EQ(one[1], 0.0f);
EXPECT_FLOAT_EQ(one[2], 0.0f);
EXPECT_FLOAT_EQ(one[3], 1.0f);
VertexAttrib2f(2, 7.0f, 8.0f);
GLfloat two[4] = {};
GetVertexAttribfv(2, GL_CURRENT_VERTEX_ATTRIB, two);
EXPECT_FLOAT_EQ(two[1], 8.0f);
EXPECT_FLOAT_EQ(two[2], 0.0f);
EXPECT_FLOAT_EQ(two[3], 1.0f);
VertexAttrib3f(3, 7.0f, 8.0f, 9.0f);
GLfloat three[4] = {};
GetVertexAttribfv(3, GL_CURRENT_VERTEX_ATTRIB, three);
EXPECT_FLOAT_EQ(three[2], 9.0f);
EXPECT_FLOAT_EQ(three[3], 1.0f);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The integer current-value views must survive a round trip without going through float.
TEST_F(GeneralVertexArrayTest, General_CurrentVertexAttribIntegerRoundTrip) {
CreateVAO();
VertexAttribI4i(1, -5, 6, -7, 8);
GLint signedValues[4] = {};
GetVertexAttribIiv(1, GL_CURRENT_VERTEX_ATTRIB, signedValues);
EXPECT_EQ(signedValues[0], -5);
EXPECT_EQ(signedValues[1], 6);
EXPECT_EQ(signedValues[2], -7);
EXPECT_EQ(signedValues[3], 8);
VertexAttribI4ui(2, 10u, 20u, 30u, 40u);
GLuint unsignedValues[4] = {};
GetVertexAttribIuiv(2, GL_CURRENT_VERTEX_ATTRIB, unsignedValues);
EXPECT_EQ(unsignedValues[0], 10u);
EXPECT_EQ(unsignedValues[3], 40u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The GL_CURRENT_VERTEX_ATTRIB branch returned before any index validation, so an out-of-range
// index silently read past the current-value array instead of raising GL_INVALID_VALUE.
TEST_F(GeneralVertexArrayTest, General_CurrentVertexAttribQueryRejectsOutOfRangeIndex) {
CreateVAO();
const GLuint outOfRange = VertexArrayImpl::GetMaxVertexAttribs();
GLfloat floats[4] = {-1.0f, -2.0f, -3.0f, -4.0f};
GetVertexAttribfv(outOfRange, GL_CURRENT_VERTEX_ATTRIB, floats);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_FLOAT_EQ(floats[0], -1.0f);
EXPECT_FLOAT_EQ(floats[3], -4.0f);
GLint ints[4] = {-1, -2, -3, -4};
GetVertexAttribiv(outOfRange, GL_CURRENT_VERTEX_ATTRIB, ints);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_EQ(ints[0], -1);
GLint signedInts[4] = {-1, -2, -3, -4};
GetVertexAttribIiv(outOfRange, GL_CURRENT_VERTEX_ATTRIB, signedInts);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_EQ(signedInts[0], -1);
GLuint uints[4] = {1u, 2u, 3u, 4u};
GetVertexAttribIuiv(outOfRange, GL_CURRENT_VERTEX_ATTRIB, uints);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_EQ(uints[0], 1u);
}
// ---- newly-implemented glVertexAttrib* current-value setter funnels -------------------------------
// Family A: the d/s/bv/iv/uiv/usv forms are value-preserving, NOT normalized; and the short/scalar
// forms fill omitted components with (0,0,1).
TEST_F(GeneralVertexArrayTest, CurrentAttrib_NonNormalizedValuePreserving) {
CreateVAO();
GLfloat out[4];
// 3-component short: 32767 must stay 32767.0f (proves no normalization), w filled to 1.
VertexAttrib3s(1, -5, 0, 32767);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], -5.0f);
EXPECT_FLOAT_EQ(out[1], 0.0f);
EXPECT_FLOAT_EQ(out[2], 32767.0f);
EXPECT_FLOAT_EQ(out[3], 1.0f);
// 4-component byte vector: w comes from v[3], not forced to 1.
const GLbyte bytes[4] = {1, 2, 3, 4};
VertexAttrib4bv(1, bytes);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 1.0f);
EXPECT_FLOAT_EQ(out[3], 4.0f);
// ushort 65535 must NOT normalize to 1.0.
const GLushort ushorts[4] = {65535, 0, 0, 0};
VertexAttrib4usv(1, ushorts);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 65535.0f);
// 1-component double: fills (0,0,1).
VertexAttrib1d(1, 0.5);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 0.5f);
EXPECT_FLOAT_EQ(out[1], 0.0f);
EXPECT_FLOAT_EQ(out[2], 0.0f);
EXPECT_FLOAT_EQ(out[3], 1.0f);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Family B: GL 3.3 Core signed normalization is (2c+1)/(2^b-1) -- maps the full range to exactly
// [-1,1] (byte -128 -> -1, 127 -> +1) and cannot represent 0 exactly (0 -> 1/255). This is the test
// that fails against the GL 4.2 c/(2^(b-1)-1) rule.
TEST_F(GeneralVertexArrayTest, CurrentAttrib_SignedNormalizedUsesGl33Formula) {
CreateVAO();
GLfloat out[4];
const GLbyte extremes[4] = {-128, 127, 0, 127};
VertexAttrib4Nbv(1, extremes);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], -1.0f); // exact, no clamp
EXPECT_FLOAT_EQ(out[1], 1.0f); // exact
EXPECT_FLOAT_EQ(out[2], 1.0f / 255.0f); // 0 -> 1/255, NOT 0.0
EXPECT_FLOAT_EQ(out[3], 1.0f);
const GLshort sExtremes[4] = {-32768, 32767, 0, 0};
VertexAttrib4Nsv(1, sExtremes);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], -1.0f);
EXPECT_FLOAT_EQ(out[1], 1.0f);
// 32-bit signed: endpoints must be exact -- fails if computed in float or if 2*INT_MAX overflows.
const GLint iExtremes[4] = {INT_MIN, INT_MAX, 0, 0};
VertexAttrib4Niv(1, iExtremes);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], -1.0f);
EXPECT_FLOAT_EQ(out[1], 1.0f);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Family B unsigned normalization is unchanged across versions: c/(2^b-1), 0 -> 0, max -> 1.
TEST_F(GeneralVertexArrayTest, CurrentAttrib_UnsignedNormalized) {
CreateVAO();
GLfloat out[4];
const GLushort us[4] = {0, 65535, 0, 0};
VertexAttrib4Nusv(1, us);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 0.0f);
EXPECT_FLOAT_EQ(out[1], 1.0f);
// 32-bit unsigned endpoints must be exact (needs double divisor).
const GLuint ui[4] = {0u, 0xFFFFFFFFu, 0u, 0u};
VertexAttrib4Nuiv(1, ui);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 0.0f);
EXPECT_FLOAT_EQ(out[1], 1.0f);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Family C: the I* forms write the INTEGER view verbatim (no float round-trip), fill w with integer 1,
// and route signed/unsigned to the right setter.
TEST_F(GeneralVertexArrayTest, CurrentAttrib_IntegerFunnels) {
CreateVAO();
// Scalar signed: w must be the integer 1, not 0.
VertexAttribI1i(1, 7);
GLint iv[4];
GetVertexAttribIiv(1, GL_CURRENT_VERTEX_ATTRIB, iv);
EXPECT_EQ(iv[0], 7);
EXPECT_EQ(iv[1], 0);
EXPECT_EQ(iv[2], 0);
EXPECT_EQ(iv[3], 1);
// INT_MAX must survive verbatim -- would corrupt to 2147483648 through the float view.
const GLint big[4] = {INT_MAX, 0, 0, 0};
VertexAttribI4iv(1, big);
GetVertexAttribIiv(1, GL_CURRENT_VERTEX_ATTRIB, iv);
EXPECT_EQ(iv[0], INT_MAX);
// I4bv sign-extends into the signed view.
const GLbyte sb[4] = {-100, 1, 2, 3};
VertexAttribI4bv(1, sb);
GetVertexAttribIiv(1, GL_CURRENT_VERTEX_ATTRIB, iv);
EXPECT_EQ(iv[0], -100);
// I4ubv zero-extends into the UNSIGNED view (not the normalized-float 4Nubv path).
const GLubyte ub[4] = {200, 0, 0, 0};
VertexAttribI4ubv(1, ub);
GLuint uv[4];
GetVertexAttribIuiv(1, GL_CURRENT_VERTEX_ATTRIB, uv);
EXPECT_EQ(uv[0], 200u);
// I2uiv reads exactly 2 elements; w == 1u.
const GLuint two[2] = {5u, 6u};
VertexAttribI2uiv(1, two);
GetVertexAttribIuiv(1, GL_CURRENT_VERTEX_ATTRIB, uv);
EXPECT_EQ(uv[0], 5u);
EXPECT_EQ(uv[1], 6u);
EXPECT_EQ(uv[2], 0u);
EXPECT_EQ(uv[3], 1u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Family D: glGetVertexAttribdv reports the float-view current value as four doubles, needs no bound
// VAO, and enforces the same error rules as its float sibling.
TEST_F(GeneralVertexArrayTest, CurrentAttrib_GetVertexAttribdv) {
CreateVAO();
VertexAttrib4f(1, 0.25f, 0.5f, 0.75f, 1.0f);
GLdouble d[4] = {-1.0, -1.0, -1.0, -1.0};
GetVertexAttribdv(1, GL_CURRENT_VERTEX_ATTRIB, d);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_DOUBLE_EQ(d[0], 0.25);
EXPECT_DOUBLE_EQ(d[1], 0.5);
EXPECT_DOUBLE_EQ(d[2], 0.75);
EXPECT_DOUBLE_EQ(d[3], 1.0);
// Null params -> GL_INVALID_VALUE, nothing written.
GetVertexAttribdv(1, GL_CURRENT_VERTEX_ATTRIB, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
// Out-of-range index -> GL_INVALID_VALUE, params untouched.
GLdouble d2[4] = {9.0, 9.0, 9.0, 9.0};
GetVertexAttribdv(VertexArrayImpl::GetMaxVertexAttribs(), GL_CURRENT_VERTEX_ATTRIB, d2);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_DOUBLE_EQ(d2[0], 9.0);
}
// Family E: packed glVertexAttribP*ui. The _REV layout packs x in bits [0..9], y in [10..19],
// z in [20..29], w in [30..31]; x/y/z are 10-bit fields and w is a 2-bit field.
// E1: unsigned decode (normalized c/(2^b-1), and unnormalized cast-to-float).
TEST_F(GeneralVertexArrayTest, CurrentAttrib_PackedUnsigned) {
CreateVAO();
GLfloat out[4];
// x=1023, y=0, z=512, w=3.
const GLuint packed = 1023u | (0u << 10) | (512u << 20) | (3u << 30); // 0xE00003FF
VertexAttribP4ui(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_TRUE, packed);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FLOAT_EQ(out[0], 1.0f); // 1023/1023
EXPECT_FLOAT_EQ(out[1], 0.0f); // 0/1023
EXPECT_FLOAT_EQ(out[2], 512.0f / 1023.0f); // 10-bit divisor, NOT 1023 for w
EXPECT_FLOAT_EQ(out[3], 1.0f); // 3/3 (2-bit divisor)
// Unnormalized: the field values are cast straight to float, no scaling.
VertexAttribP4ui(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, packed);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 1023.0f);
EXPECT_FLOAT_EQ(out[2], 512.0f);
EXPECT_FLOAT_EQ(out[3], 3.0f);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// E2: signed normalization uses the GL 3.3 (2c+1)/(2^b-1) form -- z==0 -> 1/1023 (NOT 0.0), which is
// exactly what the GL 4.2 c/(2^(b-1)-1) rule would get wrong.
TEST_F(GeneralVertexArrayTest, CurrentAttrib_PackedSignedUsesGl33Formula) {
CreateVAO();
GLfloat out[4];
// x=511 (max +), y=-512 (min, 10-bit 0x200), z=0, w=1 (max + for 2-bit).
const GLuint packed = 0x1FFu | (0x200u << 10) | (0u << 20) | (1u << 30);
VertexAttribP4ui(1, GL_INT_2_10_10_10_REV, GL_TRUE, packed);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FLOAT_EQ(out[0], 1.0f); // (2*511+1)/1023 = 1023/1023
EXPECT_FLOAT_EQ(out[1], -1.0f); // (2*-512+1)/1023 = -1023/1023
EXPECT_FLOAT_EQ(out[2], 1.0f / 1023.0f); // (2*0+1)/1023 -- 4.2 rule would give 0.0
EXPECT_FLOAT_EQ(out[3], 1.0f); // (2*1+1)/3 = 3/3
// 2-bit signed minimum: w field = 0b10 = -2 -> (2*-2+1)/3 = -1.0.
VertexAttribP4ui(1, GL_INT_2_10_10_10_REV, GL_TRUE, 2u << 30);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[3], -1.0f);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// E3: P1/P2/P3 consume the first 1/2/3 components from the single packed word; unconsumed components
// take the (0,0,0,1) defaults, and a shorter call clears stale components from a prior P4 to the slot.
TEST_F(GeneralVertexArrayTest, CurrentAttrib_PackedComponentCountAndDefaults) {
CreateVAO();
GLfloat out[4];
VertexAttribP1ui(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 5u);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 5.0f);
EXPECT_FLOAT_EQ(out[1], 0.0f);
EXPECT_FLOAT_EQ(out[2], 0.0f);
EXPECT_FLOAT_EQ(out[3], 1.0f);
VertexAttribP2ui(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 5u | (7u << 10));
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[1], 7.0f);
EXPECT_FLOAT_EQ(out[2], 0.0f);
EXPECT_FLOAT_EQ(out[3], 1.0f);
VertexAttribP3ui(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 5u | (7u << 10) | (9u << 20));
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[2], 9.0f);
EXPECT_FLOAT_EQ(out[3], 1.0f);
// Set all four, then a P1 must reset z and w back to the defaults.
VertexAttribP4ui(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u | (2u << 10) | (3u << 20) | (1u << 30));
VertexAttribP1ui(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 5u);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 5.0f);
EXPECT_FLOAT_EQ(out[2], 0.0f); // not stale 3
EXPECT_FLOAT_EQ(out[3], 1.0f); // not stale 1-from-w
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// E4: type/index validation and the uiv single-word form.
TEST_F(GeneralVertexArrayTest, CurrentAttrib_PackedValidation) {
CreateVAO();
GLfloat out[4];
// A non-packed type is GL_INVALID_ENUM and changes nothing.
VertexAttribP4ui(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 42u);
VertexAttribP4ui(1, GL_INT, GL_FALSE, 7u); // GL_INT is not a legal packed type
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 42.0f); // unchanged by the failed call
// Attribute 0's current value is settable in core GL (the GL CTS state reset calls
// glVertexAttrib4f(0, ...) on every attribute and expects no error).
VertexAttribP4ui(0, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
// Out-of-range index -> GL_INVALID_VALUE.
VertexAttribP4ui(VertexArrayImpl::GetMaxVertexAttribs(), GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
// The uiv form dereferences a single packed word.
const GLuint word = 1023u; // x=1023 normalized -> 1.0
VertexAttribP1uiv(2, GL_UNSIGNED_INT_2_10_10_10_REV, GL_TRUE, &word);
GetVertexAttribfv(2, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FLOAT_EQ(out[0], 1.0f);
// Null uiv pointer -> GL_INVALID_VALUE.
VertexAttribP4uiv(1, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
}
// ---- packed / GL_BGRA vertex ARRAY format (glVertexAttribPointer) --------------------------------
// F1: a 2_10_10_10 array format with size 4 is stored verbatim (normalized or not).
TEST_F(GeneralVertexArrayTest, ArrayFormat_PackedStored) {
CreateVAO();
CreateVBO(GL_ARRAY_BUFFER, 64);
VertexAttribPointer(0, 4, GL_INT_2_10_10_10_REV, GL_TRUE, 0, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
const auto& a0 = MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(0);
EXPECT_EQ(a0.Size, 4);
EXPECT_EQ(a0.Type, DataType::Int2101010Rev);
EXPECT_TRUE(a0.Normalized);
EXPECT_FALSE(a0.IsBgra);
EXPECT_FALSE(a0.IsInteger);
VertexAttribPointer(1, 4, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 0, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
const auto& a1 = MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(1);
EXPECT_EQ(a1.Type, DataType::Uint2101010Rev);
EXPECT_FALSE(a1.Normalized);
}
// F2: GL_BGRA is stored as size 4 with the IsBgra flag set, for GL_UNSIGNED_BYTE and 2_10_10_10.
TEST_F(GeneralVertexArrayTest, ArrayFormat_BgraStored) {
CreateVAO();
CreateVBO(GL_ARRAY_BUFFER, 64);
VertexAttribPointer(0, GL_BGRA, GL_UNSIGNED_BYTE, GL_TRUE, 0, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
const auto& a0 = MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(0);
EXPECT_EQ(a0.Size, 4); // GL_BGRA is stored as 4 components
EXPECT_EQ(a0.Type, DataType::Uint8);
EXPECT_TRUE(a0.IsBgra);
VertexAttribPointer(1, GL_BGRA, GL_INT_2_10_10_10_REV, GL_TRUE, 0, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
const auto& a1 = MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(1);
EXPECT_EQ(a1.Size, 4);
EXPECT_TRUE(a1.IsBgra);
EXPECT_EQ(a1.Type, DataType::Int2101010Rev);
}
// F3: the float-path error table -- add the format, then hard-fail the illegal combinations.
TEST_F(GeneralVertexArrayTest, ArrayFormat_FloatPathErrors) {
CreateVAO();
CreateVBO(GL_ARRAY_BUFFER, 64);
// 2_10_10_10 requires size 4 or GL_BGRA -> size 3 is GL_INVALID_OPERATION.
VertexAttribPointer(0, 3, GL_INT_2_10_10_10_REV, GL_TRUE, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
// Size-range takes precedence: size 7 (packed or not) is GL_INVALID_VALUE.
VertexAttribPointer(0, 7, GL_INT_2_10_10_10_REV, GL_TRUE, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
VertexAttribPointer(0, 0, GL_FLOAT, GL_FALSE, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
// GL_BGRA requires normalized == GL_TRUE.
VertexAttribPointer(0, GL_BGRA, GL_UNSIGNED_BYTE, GL_FALSE, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
// GL_BGRA requires GL_UNSIGNED_BYTE or a 2_10_10_10 type.
VertexAttribPointer(0, GL_BGRA, GL_FLOAT, GL_TRUE, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
VertexAttribPointer(0, GL_BGRA, GL_SHORT, GL_TRUE, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
}
// F4: glVertexAttribIPointer rejects packed types (INVALID_ENUM) and GL_BGRA (INVALID_VALUE); a plain
// integer format still works.
TEST_F(GeneralVertexArrayTest, ArrayFormat_IntegerPathRejectsPackedAndBgra) {
CreateVAO();
CreateVBO(GL_ARRAY_BUFFER, 64);
VertexAttribIPointer(0, 4, GL_INT_2_10_10_10_REV, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
VertexAttribIPointer(0, GL_BGRA, GL_INT, 0, nullptr);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
VertexAttribIPointer(0, 4, GL_INT, 0, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
const auto& a0 = MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(0);
EXPECT_EQ(a0.Type, DataType::Int32);
EXPECT_TRUE(a0.IsInteger);
EXPECT_FALSE(a0.IsBgra);
}