mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
256 lines
15 KiB
C++
256 lines
15 KiB
C++
// MobileGL - MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h
|
|
// 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
|
|
|
|
#pragma once
|
|
#include <Includes.h>
|
|
#include "../BufferState/BufferObject.h"
|
|
#include "MG_Util/Types.h"
|
|
|
|
namespace MobileGL {
|
|
namespace MG_State {
|
|
namespace GLState {
|
|
struct VertexAttribute {
|
|
Bool Enabled = false;
|
|
int Size = 4;
|
|
DataType Type = DataType::Float32;
|
|
Bool Normalized = false;
|
|
// The RESOLVED byte distance between consecutive elements, never the raw
|
|
// glVertexAttrib*Pointer argument: a pointer call's stride 0 means "tightly
|
|
// packed" and is resolved to the element size here, so a zero that survives
|
|
// into this field can only have come from the binding model, where a zero
|
|
// VERTEX_BINDING_STRIDE means the opposite - every vertex reads the SAME
|
|
// element and the fetch address never advances (GL 4.6 core 10.3.1). Backends
|
|
// consume this verbatim; collapsing 0 back into the element size is what made
|
|
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
|
|
int Stride = 0;
|
|
SizeT Offset = 0;
|
|
Bool IsInteger = false;
|
|
// GL_BGRA vertex size: four components in reversed (B,G,R,A) memory order. Size stays 4.
|
|
// Set only by the long (L) format entry points. It is NOT implied by
|
|
// Type == Float64: VertexAttribFormat(GL_DOUBLE) also reads doubles from memory but
|
|
// asks for them *converted to float*, while VertexAttribLFormat keeps all 64 bits
|
|
// (GL 4.6 core 10.3.2). Backends have to tell the two apart, and it is what
|
|
// GL_VERTEX_ATTRIB_ARRAY_LONG reports.
|
|
Bool IsLong = false;
|
|
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
|
|
// binding-point API are resolved eagerly into the flat VertexAttribute view above, so
|
|
// backends keep consuming resolved attributes and never see binding points.
|
|
struct VertexBufferBindingPoint {
|
|
SharedPtr<BufferObject> Buffer;
|
|
SizeT Offset = 0;
|
|
// GL 4.6 core table 23.4: the initial VERTEX_BINDING_STRIDE is 16, not 0.
|
|
int Stride = 16;
|
|
Uint Divisor = 0;
|
|
};
|
|
|
|
struct VertexAttributeVersion {
|
|
Uint16 FormatVersion = 0;
|
|
Uint16 BufferVersion = 0;
|
|
Uint16 SwitchVersion = 0;
|
|
};
|
|
|
|
class VertexArrayObject {
|
|
public:
|
|
// Storage capacity, not the GL-visible limit. GL_MAX_VERTEX_ATTRIBS is reported as
|
|
// min(backend limit, MAX_VERTEX_ATTRIBS) and validated against that dynamic value;
|
|
// 32 is the width of the Uint32 attribute masks the backends pass around, so it is
|
|
// also the hard ceiling.
|
|
static constexpr int MAX_VERTEX_ATTRIBS = 32;
|
|
static constexpr int MAX_VERTEX_ATTRIB_BINDINGS = 32;
|
|
|
|
VertexArrayObject(Uint externIndex);
|
|
|
|
void EnableAttribute(Uint index);
|
|
void DisableAttribute(Uint index);
|
|
Bool IsAttributeEnabled(Uint index) const;
|
|
|
|
// `stride` is the raw glVertexAttrib*Pointer argument, reported verbatim by
|
|
// GL_VERTEX_ATTRIB_ARRAY_STRIDE. `effectiveStride` is what the fetch actually
|
|
// advances by - the same value when the argument is non-zero, the tightly
|
|
// packed element size when it is zero. Pass -1 to say the two are the same.
|
|
void SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride, SizeT offset,
|
|
Bool isInteger, Bool isBgra = false, int effectiveStride = -1);
|
|
|
|
void BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer);
|
|
|
|
// Record what the pointer-style API implies for the binding-point view: attribute
|
|
// `index` bound to binding point `index` with relative offset 0, and that binding
|
|
// point carrying the buffer, the pointer offset and the effective stride.
|
|
void MirrorPointerIntoBinding(Uint index, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
|
int effectiveStride);
|
|
|
|
BindingSlot<BufferObject>& GetIndexBufferBindingSlot();
|
|
const BindingSlot<BufferObject>& GetIndexBufferBindingSlot() const;
|
|
|
|
const VertexAttribute& GetAttribute(Uint index) const;
|
|
const Array<VertexAttribute, MAX_VERTEX_ATTRIBS>& GetAllAttributes() const;
|
|
|
|
Uint GetExternalIndex() const;
|
|
|
|
// Globally-unique, never-reused id for THIS object's lifetime - the same
|
|
// contract as ProgramObject::GetLifetimeId(), and needed for the same
|
|
// reason. Neither the GL name (freed to a LIFO list and handed straight
|
|
// back by the next glGenVertexArrays) nor the heap address (freed to the
|
|
// allocator and handed straight back by the next allocation of this size)
|
|
// can tell a deleted-and-recreated VAO from the original, so a backend
|
|
// memo keyed on either one silently inherits the dead object's contents.
|
|
// That is not hypothetical: it is what let a transform-feedback capture
|
|
// fetch a destroyed VAO's vertex buffer slice (see the VaoDrawMemo key in
|
|
// DirectVulkan's VulkanRenderer).
|
|
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
|
|
|
void SetAttributeDivisor(Uint index, Uint divisor);
|
|
Uint GetAttributeDivisor(Uint index) const;
|
|
|
|
// ARB_vertex_attrib_binding style state. Each mutation re-resolves the affected
|
|
// attributes into the flat VertexAttribute view.
|
|
void SetBindingBuffer(Uint bindingIndex, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
|
int stride);
|
|
void SetBindingDivisor(Uint bindingIndex, Uint divisor);
|
|
void SetAttributeBinding(Uint attribIndex, Uint bindingIndex);
|
|
void SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized,
|
|
Bool isInteger, Uint relativeOffset, Bool isBgra = false,
|
|
Bool isLong = false);
|
|
|
|
// The binding-point view the attributes were resolved from. Kept queryable
|
|
// because glGetVertexArrayIndexed[64]iv reports it verbatim, and the resolved
|
|
// flat attribute cannot always be inverted back into it.
|
|
Uint GetAttributeRelativeOffset(Uint attribIndex) const {
|
|
return attribIndex < m_attributeRelativeOffset.size() ? m_attributeRelativeOffset[attribIndex] : 0;
|
|
}
|
|
Uint GetAttributeBindingIndex(Uint attribIndex) const {
|
|
return attribIndex < m_attributeBindingIndex.size() ? m_attributeBindingIndex[attribIndex]
|
|
: attribIndex;
|
|
}
|
|
const VertexBufferBindingPoint& GetBindingPoint(Uint bindingIndex) const {
|
|
static const VertexBufferBindingPoint kEmpty{};
|
|
return bindingIndex < m_bindingPoints.size() ? m_bindingPoints[bindingIndex] : kEmpty;
|
|
}
|
|
|
|
const VertexAttributeVersion& GetAttributeVersion(Uint index) const;
|
|
const Array<VertexAttributeVersion, MAX_VERTEX_ATTRIBS>& GetAllAttributeVersions() const;
|
|
|
|
// Aggregate of every per-attribute version bump; lets backends detect
|
|
// "any vertex-input state changed" with one compare.
|
|
Uint32 GetConfigVersion() const { return m_configVersion; }
|
|
|
|
// Backend-owned content-hash memo, valid while the config version matches
|
|
// (same idea as ProgramObject's hash memo — avoids re-hashing all
|
|
// attributes on every draw).
|
|
Bool GetBackendHashMemo(Uint64& outHash) const {
|
|
if (m_backendHashMemoVersion != m_configVersion) return false;
|
|
outHash = m_backendHashMemo;
|
|
return true;
|
|
}
|
|
void SetBackendHashMemo(Uint64 hash) const {
|
|
m_backendHashMemo = hash;
|
|
m_backendHashMemoVersion = m_configVersion;
|
|
}
|
|
|
|
// Backend-owned resolved-state memo: an opaque pointer into the
|
|
// backend's vertex-input-state cache plus the cache's eviction
|
|
// epoch, valid while the config version matches. Lets the
|
|
// per-draw path skip the content hash AND the cache lookup; the
|
|
// epoch guards against the cache evicting the pointee.
|
|
Bool GetBackendStateMemo(const void*& outState, Uint64& outEpoch) const {
|
|
if (m_backendStateMemoVersion != m_configVersion) return false;
|
|
outState = m_backendStateMemo;
|
|
outEpoch = m_backendStateMemoEpoch;
|
|
return true;
|
|
}
|
|
void SetBackendStateMemo(const void* state, Uint64 epoch) const {
|
|
m_backendStateMemo = state;
|
|
m_backendStateMemoEpoch = epoch;
|
|
m_backendStateMemoVersion = m_configVersion;
|
|
}
|
|
|
|
// Backend-owned aux memo: two opaque VALUE words (no pointee, so unlike the
|
|
// state memo above they need no eviction-epoch guard), valid while the config
|
|
// version matches. They live next to m_configVersion, which every per-draw
|
|
// path already loads, so a backend can re-read small derived facts about this
|
|
// VAO's configuration (e.g. a layout hash and attribute masks) without
|
|
// chasing into its own cache's heap entry - that chase is a guaranteed cache
|
|
// miss when an app cycles hundreds of VAOs per frame.
|
|
Bool GetBackendAuxMemo(Uint64& outAux0, Uint64& outAux1) const {
|
|
if (m_backendAuxMemoVersion != m_configVersion) return false;
|
|
outAux0 = m_backendAuxMemo0;
|
|
outAux1 = m_backendAuxMemo1;
|
|
return true;
|
|
}
|
|
void SetBackendAuxMemo(Uint64 aux0, Uint64 aux1) const {
|
|
m_backendAuxMemo0 = aux0;
|
|
m_backendAuxMemo1 = aux1;
|
|
m_backendAuxMemoVersion = m_configVersion;
|
|
}
|
|
|
|
private:
|
|
void BumpAttributeFormatVersion(Uint index);
|
|
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
|
|
// tail mapped to binding point 0 whenever the limit grows.
|
|
static constexpr Array<Uint, MAX_VERTEX_ATTRIBS> MakeIdentityAttributeBindings() {
|
|
Array<Uint, MAX_VERTEX_ATTRIBS> mapping{};
|
|
for (Uint index = 0; index < static_cast<Uint>(MAX_VERTEX_ATTRIBS); ++index) {
|
|
mapping[index] = index;
|
|
}
|
|
return mapping;
|
|
}
|
|
|
|
static Uint64 AllocateLifetimeId();
|
|
|
|
const Uint m_externalIndex = 0;
|
|
const Uint64 m_lifetimeId = AllocateLifetimeId();
|
|
Array<VertexAttribute, MAX_VERTEX_ATTRIBS> m_attributes;
|
|
Array<VertexAttributeVersion, MAX_VERTEX_ATTRIBS> m_attributeVersions;
|
|
BindingSlot<BufferObject> m_indexBufferBindingSlot;
|
|
|
|
Array<VertexBufferBindingPoint, MAX_VERTEX_ATTRIB_BINDINGS> m_bindingPoints;
|
|
Array<Uint, MAX_VERTEX_ATTRIBS> m_attributeBindingIndex = MakeIdentityAttributeBindings();
|
|
Array<Uint, MAX_VERTEX_ATTRIBS> m_attributeRelativeOffset = {};
|
|
// Set once an attribute (or its binding point) is touched through the
|
|
// ARB_vertex_attrib_binding API; only such attributes are re-resolved, so the
|
|
// classic glVertexAttribPointer path keeps its exact historical behavior.
|
|
Array<Bool, MAX_VERTEX_ATTRIBS> m_attributeUsesBindingModel = {};
|
|
|
|
Uint32 m_configVersion = 0;
|
|
mutable Uint64 m_backendHashMemo = 0;
|
|
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
|
mutable const void* m_backendStateMemo = nullptr;
|
|
mutable Uint64 m_backendStateMemoEpoch = 0;
|
|
mutable Uint32 m_backendStateMemoVersion = ~0u;
|
|
mutable Uint64 m_backendAuxMemo0 = 0;
|
|
mutable Uint64 m_backendAuxMemo1 = 0;
|
|
mutable Uint32 m_backendAuxMemoVersion = ~0u;
|
|
};
|
|
} // namespace GLState
|
|
} // namespace MG_State
|
|
} // namespace MobileGL
|