[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:
2026-08-11 08:49:37 -04:00
parent 964a7fcc92
commit d83b4dbbb5
5 changed files with 562 additions and 37 deletions
@@ -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