mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix, Test] (MG_State, MG_Impl): ARB_vertex_attrib_binding state model - spec stride default, legacy stride/pointer shadows, divisor re-binds, core-profile VAO-0 rejection
This commit is contained in:
@@ -179,6 +179,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return vao;
|
||||
}
|
||||
|
||||
// The ARB_vertex_attrib_binding entry points that take no vertex array name modify the
|
||||
// *bound* vertex array, and in a core profile the default vertex array (name 0) is not
|
||||
// one: every one of them is INVALID_OPERATION there (GL 4.6 core 10.3.1, and the tail of
|
||||
// each KHR-GL4x.vertex_attrib_binding.negative-* case checks exactly this). MobileGL
|
||||
// keeps a real object at name 0 for the compatibility paths, so GetBoundVertexArray
|
||||
// never returns null and the rule has to be spelled out - behind the same gate the VAO-0
|
||||
// draw rule already uses (MOBILEGL_RELAXED_SEMANTICS, plus "the context never asked for
|
||||
// a core profile"), so applications that legitimately run relaxed keep working.
|
||||
static SharedPtr<MG_State::GLState::VertexArrayObject> GetBoundVertexArrayForBindingApi(const char* funcName) {
|
||||
auto vao = GetBoundVertexArrayOrError(funcName);
|
||||
if (!vao) return nullptr;
|
||||
if (vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", funcName,
|
||||
"The default vertex array object cannot be modified in a core profile."));
|
||||
return nullptr;
|
||||
}
|
||||
return vao;
|
||||
}
|
||||
|
||||
static bool ValidateVertexAttribPname(GLenum pname) {
|
||||
switch (pname) {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
||||
@@ -944,7 +966,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[0] = static_cast<GLfloat>(attr->Size);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
params[0] = static_cast<GLfloat>(attr->Stride);
|
||||
params[0] = static_cast<GLfloat>(attr->LegacyStride);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||
@@ -1014,7 +1036,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[0] = static_cast<GLdouble>(attr->Size);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
params[0] = static_cast<GLdouble>(attr->Stride);
|
||||
params[0] = static_cast<GLdouble>(attr->LegacyStride);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||
@@ -1079,8 +1101,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
||||
params[0] = attr->Size;
|
||||
return;
|
||||
// The legacy shadow, not the resolved draw stride: GL 4.6 core table 23.3 defines this
|
||||
// as the last glVertexAttrib*Pointer argument, which glBindVertexBuffer must not
|
||||
// overwrite even though it does overwrite what the backend actually reads.
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
params[0] = attr->Stride;
|
||||
params[0] = attr->LegacyStride;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||
@@ -1138,7 +1163,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const auto& attr = vao->GetAttribute(index);
|
||||
*pointer = reinterpret_cast<void*>(attr.Offset);
|
||||
*pointer = reinterpret_cast<void*>(attr.LegacyPointer);
|
||||
}
|
||||
|
||||
void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) {
|
||||
@@ -1222,7 +1247,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*param = static_cast<GLint>(attr.Size);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
*param = static_cast<GLint>(attr.Stride);
|
||||
*param = static_cast<GLint>(attr.LegacyStride);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
|
||||
@@ -1294,14 +1319,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) {
|
||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffer");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffer");
|
||||
if (!vao) return;
|
||||
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
|
||||
}
|
||||
|
||||
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
||||
const GLsizei* strides) {
|
||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffers");
|
||||
if (!vao) return;
|
||||
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
@@ -1315,21 +1340,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribFormat");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
|
||||
"VertexAttribFormat");
|
||||
}
|
||||
|
||||
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribIFormat");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribIFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
|
||||
"VertexAttribIFormat");
|
||||
}
|
||||
|
||||
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribLFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||
}
|
||||
@@ -1341,7 +1366,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribBinding");
|
||||
if (!vao) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
|
||||
@@ -1349,7 +1374,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexBindingDivisor");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexBindingDivisor");
|
||||
if (!vao) return;
|
||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
|
||||
vao->SetBindingDivisor(bindingindex, divisor);
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
attr.Normalized = false;
|
||||
attr.Stride = 0;
|
||||
attr.Offset = 0;
|
||||
attr.LegacyStride = 0;
|
||||
attr.LegacyPointer = 0;
|
||||
attr.Buffer = nullptr;
|
||||
|
||||
BumpAttributeFormatVersion(index);
|
||||
@@ -61,10 +63,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
void VertexArrayObject::SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride,
|
||||
SizeT offset, Bool isInteger, Bool isBgra) {
|
||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||
if (size < 1 || size > 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The classic pointer-style API takes back full ownership of the resolved fields.
|
||||
m_attributeUsesBindingModel[index] = false;
|
||||
|
||||
// The legacy query shadows: written here and nowhere else, so a later binding-model
|
||||
// mutation cannot leak into VERTEX_ATTRIB_ARRAY_STRIDE / _POINTER. They are pure
|
||||
// query state, so they carry no version bump of their own.
|
||||
m_attributes[index].LegacyStride = stride;
|
||||
m_attributes[index].LegacyPointer = offset;
|
||||
|
||||
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
|
||||
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
|
||||
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
|
||||
@@ -72,10 +83,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
return;
|
||||
}
|
||||
|
||||
if (size < 1 || size > 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& attr = m_attributes[index];
|
||||
attr.Size = size;
|
||||
attr.Type = type;
|
||||
@@ -111,6 +118,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
binding.Offset = offset;
|
||||
binding.Stride = effectiveStride;
|
||||
binding.Divisor = m_attributes[index].Divisor;
|
||||
|
||||
// Other attributes may already be pointed at this binding point through
|
||||
// glVertexAttribBinding; they see the new buffer/offset/stride too (basic-state3
|
||||
// checks exactly that after a glVertexAttribPointer). They are not adopted into the
|
||||
// binding model here - only the ones already in it re-resolve.
|
||||
ResolveAttributesForBinding(index, /*adopt: */ false);
|
||||
}
|
||||
|
||||
void VertexArrayObject::BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer) {
|
||||
@@ -147,10 +160,24 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) {
|
||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||
// glVertexAttribDivisor is VertexBindingDivisor on the attribute's own binding point
|
||||
// (GL 4.6 core 10.3.2), so the binding-point view has to follow the resolved attribute.
|
||||
if (index < MAX_VERTEX_ATTRIB_BINDINGS && m_attributeBindingIndex[index] == index) {
|
||||
// GL 4.6 core 10.3.2 defines VertexAttribDivisor(i, d) as
|
||||
// VertexAttribBinding(i, i); VertexBindingDivisor(i, d)
|
||||
// - the binding is RE-POINTED at i, it is not merely written through when it already
|
||||
// happens to be i. Guarding the write on "binding == index" (which is what this did)
|
||||
// left an attribute that glVertexAttribBinding had moved elsewhere pointing at the old
|
||||
// binding, so the next resolve restored that binding's divisor and the new one was
|
||||
// lost (KHR-GL4x.vertex_attrib_binding.basic-state4).
|
||||
//
|
||||
// What is deliberately NOT copied from VertexAttribBinding is the adoption into the
|
||||
// binding model: an attribute configured the classic way keeps its pointer-resolved
|
||||
// stride/offset, exactly as before. The binding point mirrors that state already
|
||||
// (MirrorPointerIntoBinding), so nothing observable differs - and adopting it here
|
||||
// would silently swap the raw pointer stride for the effective one under every
|
||||
// application that calls glVertexAttribDivisor after glVertexAttribPointer.
|
||||
if (index < MAX_VERTEX_ATTRIB_BINDINGS) {
|
||||
m_attributeBindingIndex[index] = index;
|
||||
m_bindingPoints[index].Divisor = divisor;
|
||||
ResolveAttributesForBinding(index, /*adopt: */ false);
|
||||
}
|
||||
if (m_attributes[index].Divisor == divisor) return;
|
||||
m_attributes[index].Divisor = divisor;
|
||||
@@ -164,7 +191,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
void VertexArrayObject::ResolveAttributeFromBinding(Uint attribIndex) {
|
||||
if (attribIndex >= MAX_VERTEX_ATTRIBS) return;
|
||||
if (!m_attributeUsesBindingModel[attribIndex]) return;
|
||||
|
||||
const Uint bindingIndex = m_attributeBindingIndex[attribIndex];
|
||||
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
||||
@@ -172,11 +198,24 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
auto& attr = m_attributes[attribIndex];
|
||||
|
||||
// VERTEX_ATTRIB_ARRAY_DIVISOR is not independent per-attribute state: it IS the divisor
|
||||
// of the binding point the attribute is attached to (GL 4.6 core 10.3.2), whichever API
|
||||
// configured the attribute. glVertexBindingDivisor therefore has to reach a classic
|
||||
// pointer-configured attribute as well - basic-state4 alternates the two spellings on
|
||||
// the same attribute and expects each to win in turn.
|
||||
if (attr.Divisor != binding.Divisor) {
|
||||
attr.Divisor = binding.Divisor;
|
||||
BumpAttributeFormatVersion(attribIndex);
|
||||
}
|
||||
|
||||
// Everything else stays owned by whichever API configured the attribute: a classic
|
||||
// glVertexAttrib*Pointer attribute keeps its pointer-resolved stride and offset.
|
||||
if (!m_attributeUsesBindingModel[attribIndex]) return;
|
||||
|
||||
const SizeT resolvedOffset = binding.Offset + m_attributeRelativeOffset[attribIndex];
|
||||
if (attr.Stride != binding.Stride || attr.Offset != resolvedOffset || attr.Divisor != binding.Divisor) {
|
||||
if (attr.Stride != binding.Stride || attr.Offset != resolvedOffset) {
|
||||
attr.Stride = binding.Stride;
|
||||
attr.Offset = resolvedOffset;
|
||||
attr.Divisor = binding.Divisor;
|
||||
BumpAttributeFormatVersion(attribIndex);
|
||||
}
|
||||
|
||||
@@ -186,6 +225,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
}
|
||||
|
||||
void VertexArrayObject::ResolveAttributesForBinding(Uint bindingIndex, Bool adopt) {
|
||||
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
||||
if (m_attributeBindingIndex[attribIndex] != bindingIndex) continue;
|
||||
if (adopt) m_attributeUsesBindingModel[attribIndex] = true;
|
||||
ResolveAttributeFromBinding(attribIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void VertexArrayObject::SetBindingBuffer(Uint bindingIndex, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
||||
int stride) {
|
||||
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
||||
@@ -195,15 +242,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
binding.Offset = offset;
|
||||
binding.Stride = stride;
|
||||
|
||||
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
||||
if (m_attributeBindingIndex[attribIndex] == bindingIndex) {
|
||||
// Binding a vertex buffer to a binding point adopts every attribute currently
|
||||
// mapped to that binding point into the binding model (the default mapping is
|
||||
// attribute i -> binding i, which matches the GL 4.3 rules for state mixing).
|
||||
m_attributeUsesBindingModel[attribIndex] = true;
|
||||
ResolveAttributeFromBinding(attribIndex);
|
||||
}
|
||||
}
|
||||
// Binding a vertex buffer to a binding point adopts every attribute currently mapped to
|
||||
// that binding point into the binding model (the default mapping is attribute i ->
|
||||
// binding i, which matches the GL 4.3 rules for state mixing).
|
||||
ResolveAttributesForBinding(bindingIndex, /*adopt: */ true);
|
||||
}
|
||||
|
||||
void VertexArrayObject::SetBindingDivisor(Uint bindingIndex, Uint divisor) {
|
||||
@@ -211,11 +253,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
m_bindingPoints[bindingIndex].Divisor = divisor;
|
||||
|
||||
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
||||
if (m_attributeBindingIndex[attribIndex] == bindingIndex && m_attributeUsesBindingModel[attribIndex]) {
|
||||
ResolveAttributeFromBinding(attribIndex);
|
||||
}
|
||||
}
|
||||
ResolveAttributesForBinding(bindingIndex, /*adopt: */ false);
|
||||
}
|
||||
|
||||
void VertexArrayObject::SetAttributeBinding(Uint attribIndex, Uint bindingIndex) {
|
||||
|
||||
@@ -32,6 +32,16 @@ namespace MobileGL {
|
||||
Bool IsBgra = false;
|
||||
Uint Divisor = 0;
|
||||
SharedPtr<BufferObject> Buffer;
|
||||
|
||||
// GL 4.6 core table 23.3: VERTEX_ATTRIB_ARRAY_STRIDE and _POINTER are the
|
||||
// arguments of the last glVertexAttrib*Pointer call on this attribute,
|
||||
// reported verbatim, and NOTHING else writes them - not glVertexAttribFormat,
|
||||
// not glBindVertexBuffer. Stride/Offset above are the *resolved* draw inputs
|
||||
// and the binding model does overwrite those, so the two views have to be
|
||||
// stored apart or the binding-model sequence reports a legacy state it never
|
||||
// set (KHR-GL4x.vertex_attrib_binding.basic-state3).
|
||||
int LegacyStride = 0;
|
||||
SizeT LegacyPointer = 0;
|
||||
};
|
||||
|
||||
// ARB_vertex_attrib_binding separate binding point. Attributes configured through the
|
||||
@@ -40,7 +50,8 @@ namespace MobileGL {
|
||||
struct VertexBufferBindingPoint {
|
||||
SharedPtr<BufferObject> Buffer;
|
||||
SizeT Offset = 0;
|
||||
int Stride = 0;
|
||||
// GL 4.6 core table 23.4: the initial VERTEX_BINDING_STRIDE is 16, not 0.
|
||||
int Stride = 16;
|
||||
Uint Divisor = 0;
|
||||
};
|
||||
|
||||
@@ -185,6 +196,10 @@ namespace MobileGL {
|
||||
void BumpAttributeBufferVersion(Uint index);
|
||||
void BumpAttributeSwitchVersion(Uint index);
|
||||
void ResolveAttributeFromBinding(Uint attribIndex);
|
||||
// Re-resolve every attribute currently pointed at `bindingIndex`. `adopt` turns
|
||||
// the ones that are not in the binding model yet into binding-model attributes
|
||||
// first (what glBindVertexBuffer does, GL 4.3 rules for state mixing).
|
||||
void ResolveAttributesForBinding(Uint bindingIndex, Bool adopt);
|
||||
|
||||
// The default mapping is attribute i -> binding point i. Keep it an iota over
|
||||
// MAX_VERTEX_ATTRIBS rather than a literal list: a literal list silently leaves the
|
||||
|
||||
@@ -16,5 +16,22 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_executable(
|
||||
VertexAttribBindingStateTest
|
||||
VertexAttribBindingStateTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(VertexAttribBindingStateTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
VertexAttribBindingStateTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(VertexAttribBindingStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
// MobileGL - MobileGL/MG_Test/VertexArray/VertexAttribBindingStateTest.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
|
||||
|
||||
// The ARB_vertex_attrib_binding state model, replayed exactly as
|
||||
// KHR-GL4x.vertex_attrib_binding.basic-state1/3/4 and .negative-* walk it
|
||||
// (external/openglcts/modules/gl/gl4cVertexAttribBindingTests.cpp): after each mutation the
|
||||
// ten per-attribute pnames and the four per-binding-point pnames are read back in full, which
|
||||
// is what makes a single wrong field visible as itself instead of as a downstream render
|
||||
// difference.
|
||||
//
|
||||
// Four defects are pinned here, all of them frontend-only (both backends reported them
|
||||
// byte-identically):
|
||||
// * VERTEX_BINDING_STRIDE defaulted to 0; the spec's initial value is 16.
|
||||
// * The eager binding -> attribute resolve overwrote VERTEX_ATTRIB_ARRAY_STRIDE / _POINTER,
|
||||
// which are legacy state only glVertexAttrib*Pointer may write.
|
||||
// * glVertexAttribDivisor did not re-point the attribute at its own binding point, so a
|
||||
// later resolve restored the old binding's divisor.
|
||||
// * The binding entry points accepted the default vertex array (name 0) in a core profile.
|
||||
//
|
||||
// GPU-free: this is all GL object state, no backend is consulted.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <Config.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_State/EGLState/Core.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Impl::GLImpl;
|
||||
|
||||
namespace {
|
||||
|
||||
// Mirrors the CTS's VertexAttribState: the initial per-attribute state, mutated field by
|
||||
// field as the sequence proceeds, and verified in full after every call.
|
||||
struct AttribState {
|
||||
explicit AttribState(GLuint attribIndex) : index(attribIndex), binding(attribIndex) {}
|
||||
|
||||
GLuint index = 0;
|
||||
GLint enabled = 0;
|
||||
GLint size = 4;
|
||||
GLint stride = 0;
|
||||
GLenum type = GL_FLOAT;
|
||||
GLint normalized = 0;
|
||||
GLint integer = 0;
|
||||
GLint isLong = 0;
|
||||
GLint divisor = 0;
|
||||
GLuint pointer = 0;
|
||||
GLuint bufferBinding = 0;
|
||||
GLuint binding = 0;
|
||||
GLint relativeOffset = 0;
|
||||
|
||||
void Verify(const char* where) const {
|
||||
GLint p = -1;
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &p);
|
||||
EXPECT_EQ(p, enabled) << where << ": ENABLED(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_SIZE, &p);
|
||||
EXPECT_EQ(p, size) << where << ": SIZE(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_STRIDE, &p);
|
||||
EXPECT_EQ(p, stride) << where << ": STRIDE(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_TYPE, &p);
|
||||
EXPECT_EQ(static_cast<GLenum>(p), type) << where << ": TYPE(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED, &p);
|
||||
EXPECT_EQ(p, normalized) << where << ": NORMALIZED(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_INTEGER, &p);
|
||||
EXPECT_EQ(p, integer) << where << ": INTEGER(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_LONG, &p);
|
||||
EXPECT_EQ(p, isLong) << where << ": LONG(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &p);
|
||||
EXPECT_EQ(p, divisor) << where << ": DIVISOR(" << index << ")";
|
||||
void* pp = nullptr;
|
||||
GetVertexAttribPointerv(index, GL_VERTEX_ATTRIB_ARRAY_POINTER, &pp);
|
||||
EXPECT_EQ(reinterpret_cast<uintptr_t>(pp), static_cast<uintptr_t>(pointer))
|
||||
<< where << ": POINTER(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING, &p);
|
||||
EXPECT_EQ(static_cast<GLuint>(p), bufferBinding) << where << ": BUFFER_BINDING(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_BINDING, &p);
|
||||
EXPECT_EQ(static_cast<GLuint>(p), binding) << where << ": BINDING(" << index << ")";
|
||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &p);
|
||||
EXPECT_EQ(p, relativeOffset) << where << ": RELATIVE_OFFSET(" << index << ")";
|
||||
}
|
||||
};
|
||||
|
||||
// Mirrors the CTS's VertexBindingState, initial stride 16 included.
|
||||
struct BindingState {
|
||||
explicit BindingState(GLuint bindingIndex) : index(bindingIndex) {}
|
||||
|
||||
GLuint index = 0;
|
||||
GLuint buffer = 0;
|
||||
GLint offset = 0;
|
||||
GLint stride = 16;
|
||||
GLint divisor = 0;
|
||||
|
||||
void Verify(const char* where) const {
|
||||
GLint p = -1;
|
||||
GetIntegeri_v(GL_VERTEX_BINDING_BUFFER, index, &p);
|
||||
EXPECT_EQ(static_cast<GLuint>(p), buffer) << where << ": VERTEX_BINDING_BUFFER(" << index << ")";
|
||||
// The CTS reads the offset through glGetInteger64i_v; that entry point's pname
|
||||
// routing is a separate defect with its own regression (see the indexed-getter
|
||||
// parity test), so the state model is pinned through the 32-bit view here.
|
||||
GetIntegeri_v(GL_VERTEX_BINDING_OFFSET, index, &p);
|
||||
EXPECT_EQ(p, offset) << where << ": VERTEX_BINDING_OFFSET(" << index << ")";
|
||||
GetIntegeri_v(GL_VERTEX_BINDING_STRIDE, index, &p);
|
||||
EXPECT_EQ(p, stride) << where << ": VERTEX_BINDING_STRIDE(" << index << ")";
|
||||
GetIntegeri_v(GL_VERTEX_BINDING_DIVISOR, index, &p);
|
||||
EXPECT_EQ(p, divisor) << where << ": VERTEX_BINDING_DIVISOR(" << index << ")";
|
||||
}
|
||||
};
|
||||
|
||||
// Strict core rules only apply when the current EGL context explicitly asked for a core
|
||||
// profile; the suite's default (no current context) is relaxed. RAII so a failed
|
||||
// expectation cannot leave the context current for the rest of the binary.
|
||||
struct ScopedCoreProfileContext {
|
||||
ScopedCoreProfileContext() {
|
||||
auto& egl = *MG_State::pEGLContext;
|
||||
m_display = egl.GetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
EXPECT_NE(m_display, EGL_NO_DISPLAY);
|
||||
EXPECT_TRUE(egl.InitializeDisplay(m_display, nullptr, nullptr));
|
||||
EGLint configCount = 0;
|
||||
EXPECT_TRUE(egl.ChooseConfig(m_display, nullptr, &m_config, 1, &configCount));
|
||||
const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
|
||||
m_surface = egl.CreatePbufferSurface(m_display, m_config, surfaceAttribs);
|
||||
EXPECT_NE(m_surface, EGL_NO_SURFACE);
|
||||
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION,
|
||||
3,
|
||||
EGL_CONTEXT_MINOR_VERSION,
|
||||
3,
|
||||
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
||||
EGL_NONE};
|
||||
m_context = egl.CreateContext(m_display, m_config, EGL_NO_CONTEXT, contextAttribs);
|
||||
EXPECT_NE(m_context, EGL_NO_CONTEXT);
|
||||
EXPECT_TRUE(egl.MakeCurrent(m_display, m_surface, m_surface, m_context));
|
||||
}
|
||||
~ScopedCoreProfileContext() {
|
||||
auto& egl = *MG_State::pEGLContext;
|
||||
egl.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
if (m_context != EGL_NO_CONTEXT) egl.DestroyContext(m_display, m_context);
|
||||
if (m_surface != EGL_NO_SURFACE) egl.DestroySurface(m_display, m_surface);
|
||||
}
|
||||
ScopedCoreProfileContext(const ScopedCoreProfileContext&) = delete;
|
||||
ScopedCoreProfileContext& operator=(const ScopedCoreProfileContext&) = delete;
|
||||
|
||||
private:
|
||||
EGLDisplay m_display = EGL_NO_DISPLAY;
|
||||
EGLConfig m_config = nullptr;
|
||||
EGLSurface m_surface = EGL_NO_SURFACE;
|
||||
MG_State::EGLState::EGLContext::EGLContextHandle m_context = EGL_NO_CONTEXT;
|
||||
};
|
||||
|
||||
class VertexAttribBindingStateTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
// A fresh context per case: the state model under test is cumulative, so a leftover
|
||||
// VAO binding from a neighbour would silently change what "default state" means.
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
GenVertexArrays(1, &m_vao);
|
||||
BindVertexArray(m_vao);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||
}
|
||||
|
||||
GLuint CreateVbo(GLsizeiptr size) {
|
||||
GLuint vbo = 0;
|
||||
GenBuffers(1, &vbo);
|
||||
BindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
BufferData(GL_ARRAY_BUFFER, size, nullptr, GL_DYNAMIC_COPY);
|
||||
BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
return vbo;
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && GetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// basic-state1's opening block: the initial per-attribute mapping and the per-binding-point
|
||||
// defaults, VERTEX_BINDING_STRIDE = 16 included. That check is the FIRST thing the CTS case
|
||||
// does, so a wrong default masked everything the case would have found after it.
|
||||
TEST_F(VertexAttribBindingStateTest, DefaultsMatchTheSpecInitialState) {
|
||||
for (GLuint i = 0; i < 16; ++i) {
|
||||
AttribState(i).Verify("defaults");
|
||||
BindingState(i).Verify("defaults");
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// basic-state3, verbatim: a full separate-format sequence, then a pointer call, then a
|
||||
// binding update on top of it. The legacy STRIDE/POINTER pair must stay untouched by every
|
||||
// step except the glVertexAttribPointer one, and must survive the binding update after it.
|
||||
TEST_F(VertexAttribBindingStateTest, SeparateFormatSequenceKeepsLegacyStrideAndPointerAtZero) {
|
||||
const GLuint vbo0 = CreateVbo(10000);
|
||||
const GLuint vbo1 = CreateVbo(10000);
|
||||
const GLuint vbo2 = CreateVbo(10000);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
AttribState va0(0), va2(2), va15(15);
|
||||
BindingState vb0(0), vb2(2), vb15(15);
|
||||
|
||||
VertexAttribFormat(0, 2, GL_BYTE, GL_TRUE, 16);
|
||||
va0.size = 2;
|
||||
va0.type = GL_BYTE;
|
||||
va0.normalized = 1;
|
||||
va0.relativeOffset = 16;
|
||||
va0.Verify("after glVertexAttribFormat");
|
||||
// The format call says nothing about a buffer, so binding point 0 keeps its defaults -
|
||||
// stride 16 among them.
|
||||
vb0.Verify("after glVertexAttribFormat");
|
||||
|
||||
VertexAttribIFormat(2, 3, GL_INT, 512);
|
||||
va2.size = 3;
|
||||
va2.type = GL_INT;
|
||||
va2.integer = 1;
|
||||
va2.relativeOffset = 512;
|
||||
va2.Verify("after glVertexAttribIFormat");
|
||||
vb2.Verify("after glVertexAttribIFormat");
|
||||
|
||||
BindVertexBuffer(0, vbo0, 2048, 128);
|
||||
va0.bufferBinding = vbo0;
|
||||
vb0.buffer = vbo0;
|
||||
vb0.offset = 2048;
|
||||
vb0.stride = 128;
|
||||
va0.Verify("after glBindVertexBuffer(0)");
|
||||
vb0.Verify("after glBindVertexBuffer(0)");
|
||||
|
||||
BindVertexBuffer(2, vbo2, 64, 256);
|
||||
va2.bufferBinding = vbo2;
|
||||
vb2.buffer = vbo2;
|
||||
vb2.offset = 64;
|
||||
vb2.stride = 256;
|
||||
va2.Verify("after glBindVertexBuffer(2)");
|
||||
vb2.Verify("after glBindVertexBuffer(2)");
|
||||
|
||||
// Attribute 2 moves onto binding 0 and takes that binding point's buffer with it.
|
||||
VertexAttribBinding(2, 0);
|
||||
va2.binding = 0;
|
||||
va2.bufferBinding = vbo0;
|
||||
va0.Verify("after glVertexAttribBinding(2,0)");
|
||||
vb0.Verify("after glVertexAttribBinding(2,0)");
|
||||
va2.Verify("after glVertexAttribBinding(2,0)");
|
||||
vb2.Verify("after glVertexAttribBinding(2,0)");
|
||||
|
||||
VertexAttribBinding(0, 15);
|
||||
va0.binding = 15;
|
||||
va0.bufferBinding = 0;
|
||||
va0.Verify("after glVertexAttribBinding(0,15)");
|
||||
vb0.Verify("after glVertexAttribBinding(0,15)");
|
||||
va15.Verify("after glVertexAttribBinding(0,15)");
|
||||
vb15.Verify("after glVertexAttribBinding(0,15)");
|
||||
|
||||
BindVertexBuffer(15, vbo1, 16, 32);
|
||||
va0.bufferBinding = vbo1;
|
||||
va15.bufferBinding = vbo1;
|
||||
vb15.buffer = vbo1;
|
||||
vb15.offset = 16;
|
||||
vb15.stride = 32;
|
||||
va0.Verify("after glBindVertexBuffer(15)");
|
||||
va15.Verify("after glBindVertexBuffer(15)");
|
||||
vb15.Verify("after glBindVertexBuffer(15)");
|
||||
|
||||
// The one call that IS allowed to write the legacy pair - and it also re-points the
|
||||
// attribute at its own binding point and rewrites that binding point.
|
||||
BindBuffer(GL_ARRAY_BUFFER, vbo2);
|
||||
VertexAttribPointer(0, 4, GL_UNSIGNED_BYTE, GL_FALSE, 8, reinterpret_cast<const void*>(640));
|
||||
BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
va0.size = 4;
|
||||
va0.type = GL_UNSIGNED_BYTE;
|
||||
va0.stride = 8;
|
||||
va0.pointer = 640;
|
||||
va0.relativeOffset = 0;
|
||||
va0.normalized = 0;
|
||||
va0.binding = 0;
|
||||
va0.bufferBinding = vbo2;
|
||||
vb0.buffer = vbo2;
|
||||
vb0.offset = 640;
|
||||
vb0.stride = 8;
|
||||
va2.bufferBinding = vbo2;
|
||||
va0.Verify("after glVertexAttribPointer");
|
||||
vb0.Verify("after glVertexAttribPointer");
|
||||
va2.Verify("after glVertexAttribPointer");
|
||||
va15.Verify("after glVertexAttribPointer");
|
||||
vb15.Verify("after glVertexAttribPointer");
|
||||
|
||||
// ...and a binding update on top of it leaves the legacy pair exactly where the pointer
|
||||
// call left it. This is the assertion the eager resolve used to fail.
|
||||
BindVertexBuffer(0, vbo1, 80, 24);
|
||||
vb0.buffer = vbo1;
|
||||
vb0.offset = 80;
|
||||
vb0.stride = 24;
|
||||
va0.bufferBinding = vbo1;
|
||||
va2.bufferBinding = vbo1;
|
||||
va0.Verify("after the trailing glBindVertexBuffer(0)");
|
||||
vb0.Verify("after the trailing glBindVertexBuffer(0)");
|
||||
va2.Verify("after the trailing glBindVertexBuffer(0)");
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// basic-state4: glVertexAttribDivisor is VertexAttribBinding(i,i) + VertexBindingDivisor(i,d),
|
||||
// and glVertexBindingDivisor reaches the attribute's own DIVISOR query either way.
|
||||
TEST_F(VertexAttribBindingStateTest, DivisorGoesThroughTheBindingPoint) {
|
||||
for (GLuint i = 0; i < 16; ++i) {
|
||||
AttribState va(i);
|
||||
BindingState vb(i);
|
||||
VertexAttribDivisor(i, i + 7);
|
||||
va.divisor = static_cast<GLint>(i + 7);
|
||||
vb.divisor = static_cast<GLint>(i + 7);
|
||||
va.Verify("after glVertexAttribDivisor");
|
||||
vb.Verify("after glVertexAttribDivisor");
|
||||
}
|
||||
for (GLuint i = 0; i < 16; ++i) {
|
||||
AttribState va(i);
|
||||
BindingState vb(i);
|
||||
VertexBindingDivisor(i, i);
|
||||
va.divisor = static_cast<GLint>(i);
|
||||
vb.divisor = static_cast<GLint>(i);
|
||||
va.Verify("after glVertexBindingDivisor");
|
||||
vb.Verify("after glVertexBindingDivisor");
|
||||
}
|
||||
|
||||
// Attribute 2 moves onto binding 5 and inherits binding 5's divisor; binding 2 keeps its
|
||||
// own.
|
||||
VertexAttribBinding(2, 5);
|
||||
AttribState va5(5);
|
||||
va5.divisor = 5;
|
||||
BindingState vb5(5);
|
||||
vb5.divisor = 5;
|
||||
AttribState va2(2);
|
||||
va2.divisor = 5;
|
||||
va2.binding = 5;
|
||||
BindingState vb2(2);
|
||||
vb2.divisor = 2;
|
||||
va5.Verify("after glVertexAttribBinding(2,5)");
|
||||
vb5.Verify("after glVertexAttribBinding(2,5)");
|
||||
va2.Verify("after glVertexAttribBinding(2,5)");
|
||||
vb2.Verify("after glVertexAttribBinding(2,5)");
|
||||
|
||||
// ...and glVertexAttribDivisor pulls it back onto binding 2. Guarding the write on
|
||||
// "binding already == index" left the attribute on binding 5 and threw the divisor away.
|
||||
VertexAttribDivisor(2, 23);
|
||||
va2.binding = 2;
|
||||
va2.divisor = 23;
|
||||
vb2.divisor = 23;
|
||||
va5.Verify("after glVertexAttribDivisor(2,23)");
|
||||
vb5.Verify("after glVertexAttribDivisor(2,23)");
|
||||
va2.Verify("after glVertexAttribDivisor(2,23)");
|
||||
vb2.Verify("after glVertexAttribDivisor(2,23)");
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The tail of every negative-* case: with the default vertex array bound, a core profile
|
||||
// rejects all four binding entry points.
|
||||
TEST_F(VertexAttribBindingStateTest, BindingApiRejectsTheDefaultVertexArrayInCoreProfile) {
|
||||
ScopedCoreProfileContext coreContext;
|
||||
ASSERT_FALSE(MG_State::IsRelaxedSemanticsActive());
|
||||
DrainErrors();
|
||||
|
||||
BindVertexArray(0);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
BindVertexBuffer(0, 7, 0, 12);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glBindVertexBuffer";
|
||||
VertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribFormat";
|
||||
VertexAttribIFormat(0, 4, GL_INT, 0);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribIFormat";
|
||||
VertexAttribBinding(0, 0);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribBinding";
|
||||
VertexBindingDivisor(0, 1);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexBindingDivisor";
|
||||
|
||||
BindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// ...and the relaxed default - which is what every context that never asked for a core
|
||||
// profile gets - keeps accepting them, because applications depend on it.
|
||||
TEST_F(VertexAttribBindingStateTest, BindingApiStillAcceptsTheDefaultVertexArrayWhenRelaxed) {
|
||||
ASSERT_TRUE(MG_State::IsRelaxedSemanticsActive());
|
||||
const GLuint vbo = CreateVbo(1024);
|
||||
DrainErrors();
|
||||
|
||||
BindVertexArray(0);
|
||||
BindVertexBuffer(0, vbo, 0, 12);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glBindVertexBuffer under relaxed semantics";
|
||||
VertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexAttribFormat under relaxed semantics";
|
||||
VertexAttribBinding(0, 0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexAttribBinding under relaxed semantics";
|
||||
VertexBindingDivisor(0, 1);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexBindingDivisor under relaxed semantics";
|
||||
|
||||
BindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// MOBILEGL_RELAXED_SEMANTICS wins even on an explicit core-profile context.
|
||||
TEST_F(VertexAttribBindingStateTest, RelaxedSemanticsOverrideReopensTheDefaultVertexArray) {
|
||||
ScopedCoreProfileContext coreContext;
|
||||
const Bool saved = MG_Config::Features.RelaxedSemantics;
|
||||
MG_Config::Features.RelaxedSemantics = true;
|
||||
const GLuint vbo = CreateVbo(1024);
|
||||
DrainErrors();
|
||||
|
||||
BindVertexArray(0);
|
||||
BindVertexBuffer(0, vbo, 0, 12);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
BindVertexArray(m_vao);
|
||||
MG_Config::Features.RelaxedSemantics = saved;
|
||||
DrainErrors();
|
||||
}
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user