mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Perf] (MG_Backend): stop DirectGLES re-resolving the same VAO's buffers and twins every draw
Four per-draw costs, all lookups that re-answer the same question. SyncNeccessaryBuffers walked all 32 attribute slots cold and ran EnsureBufferResource per buffer on every draw. The backend VAO twin now hosts a resolved-draw-buffers memo: the deduped enabled-attribute buffers and the index buffer resolve once per VAO config version, and each hit re-validates every entry with IsBufferDrawClean - a shadow probe mirroring every no-op branch of EnsureBufferResource (resource identity, context generation, pending ops, change serial) - falling back to the full path for just the dirty entries. The IBO entry is checked against the live bound object each draw, so slot-version wrap cannot false-hit. The registry hash Finds that resolve state objects to their backend twins ran several times per draw. TwinLookupMemo - a direct-mapped, Fibonacci-hashed table (4096 VAO / 256 program slots) with weak-ptr owner equality against address reuse - answers them in one probe; collisions fall back to the registry. A live entry's twin is never replaced once set, so owner equality proves the raw pointer. SyncCurrentVertexAttributeValues' pending-mask memo was a function-static single entry that missed every draw once the app cycled VAOs; it now lives on the twin. CurrentXfb()'s per-draw FastSTL map lookup became a cached pointer invalidated at every map mutation (open addressing moves values on any insert/erase/clear). mc_vanilla_draw -12.6% (3.4x native to 3.0x), ubo_range -9.8%, sampler_churn -7.5%, pass_switch -6.5%, sodium_multidraw -6.0%, state_toggle -4.6%. All nine cases measured on both backends, interleaved A/B; no case regressed. Unit tests 421/421.
This commit is contained in:
@@ -60,6 +60,66 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return !snapshot.owner_before(current) && !current.owner_before(snapshot);
|
return !snapshot.owner_before(current) && !current.owner_before(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Owner-keyed direct-mapped memo over a StateBackendObjectRegistry, for the object
|
||||||
|
// kinds the draw path re-Finds every draw (VAO, program). A hit replaces the
|
||||||
|
// registry's hash-map Find with one array index plus an owner-equality compare.
|
||||||
|
//
|
||||||
|
// Why the raw twin pointer is safe to hand back: the twin is owned by the registry
|
||||||
|
// entry's SharedPtr, and a LIVE state object's entry is never erased nor has its
|
||||||
|
// twin replaced once set — Find and CollectGarbage erase only expired entries,
|
||||||
|
// GetOrCreate resets the twin only when the previous owner at that address expired,
|
||||||
|
// and the sync paths create a twin only when the slot is null. Owner-equality of
|
||||||
|
// the weak snapshot with the live frontend therefore proves the memoed pointer is
|
||||||
|
// still that frontend's registered twin. A recycled heap address fails the
|
||||||
|
// owner-equality (the weak snapshot pins the predecessor's control block), and a
|
||||||
|
// slot collision merely falls back to the registry Find. Nothing here needs an
|
||||||
|
// explicit invalidation hook: registry entries survive ES-context loss (twins
|
||||||
|
// re-sync via their own generation gates), matching the Find they replace.
|
||||||
|
template <typename StateObject, typename BackendObject, SizeT kIndexBits>
|
||||||
|
class TwinLookupMemo {
|
||||||
|
public:
|
||||||
|
BackendObject* Lookup(const SharedPtr<StateObject>& stateObj) const {
|
||||||
|
const Slot& slot = m_slots[IndexFor(stateObj.get())];
|
||||||
|
if (slot.key == stateObj.get() && slot.twin != nullptr && OwnerEquals(slot.owner, stateObj)) {
|
||||||
|
return slot.twin;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Store(const SharedPtr<StateObject>& stateObj, BackendObject* twin) {
|
||||||
|
Slot& slot = m_slots[IndexFor(stateObj.get())];
|
||||||
|
slot.key = stateObj.get();
|
||||||
|
slot.owner = stateObj;
|
||||||
|
slot.twin = twin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot {
|
||||||
|
StateObject* key = nullptr;
|
||||||
|
WeakPtr<StateObject> owner;
|
||||||
|
BackendObject* twin = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
static SizeT IndexFor(const StateObject* ptr) {
|
||||||
|
// Fibonacci hashing: heap addresses share alignment zeros and arena locality;
|
||||||
|
// the multiply spreads them before the top bits pick the slot.
|
||||||
|
const Uint64 h = reinterpret_cast<std::uintptr_t>(ptr) * 0x9E3779B97F4A7C15ull;
|
||||||
|
return static_cast<SizeT>(h >> (64u - kIndexBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
Array<Slot, (SizeT(1) << kIndexBits)> m_slots;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4096 slots (128 KiB, sparse-touched): Minecraft-shaped workloads cycle hundreds
|
||||||
|
// of section VAOs per frame (the driver bench cycles 512), and a colliding pair
|
||||||
|
// ping-pongs back onto the hash Find every frame — at 512 keys the expected
|
||||||
|
// collided fraction is ~12% here vs ~22% at 2048. Programs are far fewer; 256
|
||||||
|
// slots is plenty.
|
||||||
|
static TwinLookupMemo<MG_State::GLState::VertexArrayObject, VertexArrayImpl::BackendVertexArrayObject, 12>
|
||||||
|
g_vaoTwinLookupMemo;
|
||||||
|
static TwinLookupMemo<MG_State::GLState::ProgramObject, PrgramImpl::BackendProgramObjectImpl, 8>
|
||||||
|
g_programTwinLookupMemo;
|
||||||
|
|
||||||
static Bool IsDualSourceBlendFactor(BlendFactor v) {
|
static Bool IsDualSourceBlendFactor(BlendFactor v) {
|
||||||
switch (v) {
|
switch (v) {
|
||||||
case BlendFactor::Src1Color:
|
case BlendFactor::Src1Color:
|
||||||
@@ -263,12 +323,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// TODO: deletion for deleted objects
|
// TODO: deletion for deleted objects
|
||||||
|
|
||||||
namespace BufferImpl {
|
namespace BufferImpl {
|
||||||
void CreateAndSyncBufferObject(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
|
|
||||||
// Immediate BufferBackendOps keep existing storage current; this only
|
|
||||||
// needs to materialize the resource (and replay pending ops).
|
|
||||||
EnsureBufferResource(bufferObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SyncBufferBindingPoints(BufferTarget target, GLenum glTarget) {
|
void SyncBufferBindingPoints(BufferTarget target, GLenum glTarget) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -336,7 +390,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
BindBufferId(glTarget, backendResource->id);
|
BindBufferId(glTarget, backendResource->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SyncNeccessaryBuffers(Bool includeIBO = false, Bool includeIndirectBuffer = false) {
|
void SyncNeccessaryBuffers(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
|
||||||
|
VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Bool includeIBO = false,
|
||||||
|
Bool includeIndirectBuffer = false) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
@@ -346,44 +402,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed)
|
// 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed)
|
||||||
// PBO is not needed since it should be handled in frontend
|
// PBO is not needed since it should be handled in frontend
|
||||||
|
|
||||||
const auto& currentVAOObject = MG_State::pGLContext->GetBoundVertexArray();
|
|
||||||
if (!currentVAOObject) {
|
if (!currentVAOObject) {
|
||||||
MGLOG_E("No VAO is currently bound, cannot sync necessary buffers.");
|
MGLOG_E("No VAO is currently bound, cannot sync necessary buffers.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VBO. Once per distinct buffer, not once per enabled attribute: an interleaved
|
// VBO. The distinct-buffer set is memoed on the VAO's backend twin: cycling
|
||||||
// Minecraft-shaped VAO feeds all of its attributes out of one VBO, so the
|
// hundreds of section VAOs re-walked 32 cold VertexAttribute slots per draw
|
||||||
// per-attribute loop re-ran the whole resource check (context generation, pending
|
// only to rediscover the same one or two static buffers. While the config
|
||||||
// ranges, change serial) 4-8 times over the same object. Repeats were pure
|
// version holds (every attach/enable/disable bumps it, and it pins each
|
||||||
// overhead - nothing between two calls can change what the second one would do.
|
// memoed frontend pointer via the attribute SharedPtrs), the walk reduces to
|
||||||
const MG_State::GLState::BufferObject*
|
// an IsBufferDrawClean probe per distinct buffer; only dirty entries take
|
||||||
syncedBuffers[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS];
|
// EnsureBufferResource, re-fetched through their attribute index.
|
||||||
Uint syncedBufferCount = 0;
|
auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr;
|
||||||
for (const auto& attrib : currentVAOObject->GetAllAttributes()) {
|
const Uint32 configVersion = currentVAOObject->GetConfigVersion();
|
||||||
if (!attrib.Enabled) continue;
|
if (memo && memo->valid && memo->configVersion == configVersion) {
|
||||||
const auto& bufferObject = attrib.Buffer;
|
for (Uint i = 0; i < memo->count; ++i) {
|
||||||
if (!bufferObject) continue;
|
auto& entry = memo->entries[i];
|
||||||
|
if (IsBufferDrawClean(entry.frontend, entry.resource)) continue;
|
||||||
const auto* bufferKey = bufferObject.get();
|
// Same object the entry was built from: config version unchanged.
|
||||||
Bool alreadySynced = false;
|
entry.resource = EnsureBufferResource(currentVAOObject->GetAttribute(entry.attribIndex).Buffer);
|
||||||
for (Uint i = 0; i < syncedBufferCount; ++i) {
|
|
||||||
if (syncedBuffers[i] == bufferKey) {
|
|
||||||
alreadySynced = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (alreadySynced) continue;
|
} else {
|
||||||
|
// Full walk, once per distinct buffer (an interleaved Minecraft-shaped VAO
|
||||||
|
// feeds all attributes out of one VBO), rebuilding the memo as it goes.
|
||||||
|
// The twin can only be absent on the error path that has no VAO twin at
|
||||||
|
// all; the dedupe scratch keeps this branch correct even then.
|
||||||
|
MG_State::GLState::BufferObject*
|
||||||
|
syncedBuffers[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS];
|
||||||
|
Uint syncedBufferCount = 0;
|
||||||
|
const auto& allAttributes = currentVAOObject->GetAllAttributes();
|
||||||
|
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
|
||||||
|
const auto& attrib = allAttributes[attribIndex];
|
||||||
|
if (!attrib.Enabled) continue;
|
||||||
|
const auto& bufferObject = attrib.Buffer;
|
||||||
|
if (!bufferObject) continue;
|
||||||
|
|
||||||
syncedBuffers[syncedBufferCount++] = bufferKey;
|
auto* const bufferKey = bufferObject.get();
|
||||||
CreateAndSyncBufferObject(bufferObject);
|
Bool alreadySynced = false;
|
||||||
|
for (Uint i = 0; i < syncedBufferCount; ++i) {
|
||||||
|
if (syncedBuffers[i] == bufferKey) {
|
||||||
|
alreadySynced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alreadySynced) continue;
|
||||||
|
|
||||||
|
auto* resource = EnsureBufferResource(bufferObject);
|
||||||
|
if (memo) {
|
||||||
|
auto& entry = memo->entries[syncedBufferCount];
|
||||||
|
entry.frontend = bufferKey;
|
||||||
|
entry.attribIndex = static_cast<Uint8>(attribIndex);
|
||||||
|
entry.resource = resource;
|
||||||
|
}
|
||||||
|
syncedBuffers[syncedBufferCount++] = bufferKey;
|
||||||
|
}
|
||||||
|
if (memo) {
|
||||||
|
memo->count = syncedBufferCount;
|
||||||
|
memo->configVersion = configVersion;
|
||||||
|
memo->valid = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IBO
|
// IBO, memoed by bound-object identity (its slot version is not covered by
|
||||||
|
// the config version). A stale identity hit is impossible in effect: the
|
||||||
|
// clean probe re-validates the resource against the LIVE bound object.
|
||||||
if (includeIBO) {
|
if (includeIBO) {
|
||||||
auto& possibleIBO = currentVAOObject->GetIndexBufferBindingSlot().GetBoundObject();
|
const auto& possibleIBO = currentVAOObject->GetIndexBufferBindingSlot().GetBoundObject();
|
||||||
if (possibleIBO) {
|
if (possibleIBO) {
|
||||||
CreateAndSyncBufferObject(possibleIBO);
|
if (!memo || memo->iboFrontend != possibleIBO.get() ||
|
||||||
|
!IsBufferDrawClean(memo->iboFrontend, memo->iboResource)) {
|
||||||
|
auto* resource = EnsureBufferResource(possibleIBO);
|
||||||
|
if (memo) {
|
||||||
|
memo->iboFrontend = possibleIBO.get();
|
||||||
|
memo->iboResource = resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,9 +567,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
UnorderedMap<GLuint, XfbObjectState> g_xfbObjects;
|
UnorderedMap<GLuint, XfbObjectState> g_xfbObjects;
|
||||||
GLuint g_currentXfbName = 0;
|
GLuint g_currentXfbName = 0;
|
||||||
|
// Cached address of g_xfbObjects[g_currentXfbName]: PrepareForDraw consults
|
||||||
|
// CurrentXfb on EVERY draw (StartPendingTransformFeedback) and the map
|
||||||
|
// lookup was pure per-draw overhead for the overwhelmingly common no-capture
|
||||||
|
// case. FastSTL's open addressing keeps values in the bucket array, so ANY
|
||||||
|
// insert can rehash and move them (and erase/clear can too): every site that
|
||||||
|
// mutates the map or rebinds the current name resets this to null instead of
|
||||||
|
// reasoning about stability, and CurrentXfb re-resolves lazily.
|
||||||
|
XfbObjectState* g_currentXfbState = nullptr;
|
||||||
|
|
||||||
XfbObjectState& CurrentXfb() {
|
XfbObjectState& CurrentXfb() {
|
||||||
return g_xfbObjects[g_currentXfbName];
|
if (g_currentXfbState == nullptr) {
|
||||||
|
g_currentXfbState = &g_xfbObjects[g_currentXfbName];
|
||||||
|
}
|
||||||
|
return *g_currentXfbState;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool AreTransformFeedbackObjectsSupported() {
|
Bool AreTransformFeedbackObjectsSupported() {
|
||||||
@@ -711,6 +816,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BindTransformFeedback(GLuint name) {
|
void BindTransformFeedback(GLuint name) {
|
||||||
|
g_currentXfbState = nullptr; // name changes; operator[] below may also rehash
|
||||||
if (!AreTransformFeedbackObjectsSupported()) {
|
if (!AreTransformFeedbackObjectsSupported()) {
|
||||||
// Without driver objects there is only the default span; keep the frontend
|
// Without driver objects there is only the default span; keep the frontend
|
||||||
// name so the bookkeeping below stays consistent.
|
// name so the bookkeeping below stays consistent.
|
||||||
@@ -731,6 +837,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (it->second.esId != 0 && g_GLESFuncs.glDeleteTransformFeedbacks != nullptr) {
|
if (it->second.esId != 0 && g_GLESFuncs.glDeleteTransformFeedbacks != nullptr) {
|
||||||
g_GLESFuncs.glDeleteTransformFeedbacks(1, &it->second.esId);
|
g_GLESFuncs.glDeleteTransformFeedbacks(1, &it->second.esId);
|
||||||
}
|
}
|
||||||
|
g_currentXfbState = nullptr; // erase can move values (open addressing)
|
||||||
g_xfbObjects.erase(it);
|
g_xfbObjects.erase(it);
|
||||||
// The frontend reverts to the default object when the bound one is deleted.
|
// The frontend reverts to the default object when the bound one is deleted.
|
||||||
if (g_currentXfbName == name) {
|
if (g_currentXfbName == name) {
|
||||||
@@ -741,6 +848,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// The ES context went away (or is being torn down): the spans, their buffer ids, the
|
// The ES context went away (or is being torn down): the spans, their buffer ids, the
|
||||||
// driver objects and the frontend objects they pinned all belonged to it.
|
// driver objects and the frontend objects they pinned all belonged to it.
|
||||||
void OnBackendContextDestroyed() {
|
void OnBackendContextDestroyed() {
|
||||||
|
g_currentXfbState = nullptr;
|
||||||
g_xfbObjects.clear();
|
g_xfbObjects.clear();
|
||||||
g_currentXfbName = 0;
|
g_currentXfbName = 0;
|
||||||
g_scatterBufferId = 0;
|
g_scatterBufferId = 0;
|
||||||
@@ -749,25 +857,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
} // namespace XfbImpl
|
} // namespace XfbImpl
|
||||||
|
|
||||||
namespace VertexArrayImpl {
|
namespace VertexArrayImpl {
|
||||||
void SyncCurrentVAO() {
|
// Resolve-or-create the VAO's backend twin, once per draw: PrepareForDraw passes
|
||||||
|
// the result to the buffer sync (resolved-buffers memo host), the VAO sync and
|
||||||
|
// the draw-time bind, which each used to run their own registry Find. The raw
|
||||||
|
// pointer stays valid for the whole draw: the frontend VAO is pinned by the
|
||||||
|
// context binding, and a live object's registry entry is never erased nor its
|
||||||
|
// twin replaced (see TwinLookupMemo's contract).
|
||||||
|
BackendVertexArrayObject* ResolveVaoTwin(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao) {
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
#endif
|
||||||
|
if (auto* twin = g_vaoTwinLookupMemo.Lookup(vao)) {
|
||||||
|
return twin;
|
||||||
|
}
|
||||||
|
auto* backendVAOSlot = g_backendVertexArrayObjects.Find(vao.get());
|
||||||
|
auto& backendObj = backendVAOSlot ? *backendVAOSlot : g_backendVertexArrayObjects.GetOrCreate(vao);
|
||||||
|
if (!backendObj) {
|
||||||
|
backendObj = MakeShared<BackendVertexArrayObject>();
|
||||||
|
}
|
||||||
|
g_vaoTwinLookupMemo.Store(vao, backendObj.get());
|
||||||
|
return backendObj.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncCurrentVAO(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
|
||||||
|
BackendVertexArrayObject* vaoTwin) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
g_backendVertexArrayObjects.CollectGarbageIfNeeded();
|
g_backendVertexArrayObjects.CollectGarbageIfNeeded();
|
||||||
|
|
||||||
auto& currentVAOObject = MG_State::pGLContext->GetBoundVertexArray();
|
if (!currentVAOObject || !vaoTwin) {
|
||||||
if (!currentVAOObject) {
|
|
||||||
MGLOG_E("No VAO is currently bound, cannot sync current VAO.");
|
MGLOG_E("No VAO is currently bound, cannot sync current VAO.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* backendVAOSlot = g_backendVertexArrayObjects.Find(currentVAOObject.get());
|
vaoTwin->SyncToBackend(currentVAOObject);
|
||||||
auto& backendObj =
|
|
||||||
backendVAOSlot ? *backendVAOSlot : g_backendVertexArrayObjects.GetOrCreate(currentVAOObject);
|
|
||||||
if (!backendObj) {
|
|
||||||
backendObj = MakeShared<VertexArrayImpl::BackendVertexArrayObject>();
|
|
||||||
}
|
|
||||||
backendObj->SyncToBackend(currentVAOObject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL: a shader input whose generic attribute array is DISABLED reads that attribute's *current
|
// GL: a shader input whose generic attribute array is DISABLED reads that attribute's *current
|
||||||
@@ -776,7 +900,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// the shader its own current values, which MobileGL never writes -- i.e. always (0,0,0,1).
|
// the shader its own current values, which MobileGL never writes -- i.e. always (0,0,0,1).
|
||||||
// SyncToBackend has already issued glDisableVertexAttribArray for these locations, so the ES
|
// SyncToBackend has already issued glDisableVertexAttribArray for these locations, so the ES
|
||||||
// current value is what the shader will actually read.
|
// current value is what the shader will actually read.
|
||||||
void SyncCurrentVertexAttributeValues() {
|
void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
@@ -784,39 +908,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (!program) return;
|
if (!program) return;
|
||||||
|
|
||||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
if (!vao) return;
|
if (!vao || !vaoTwin) return;
|
||||||
|
|
||||||
const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask();
|
const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask();
|
||||||
if (activeAttribMask == 0) return;
|
if (activeAttribMask == 0) return;
|
||||||
|
|
||||||
// Which of the program's attributes lack an enabled array cannot change without
|
// Which of the program's attributes lack an enabled array cannot change without
|
||||||
// the VAO's config version moving (Enable/DisableAttribute bump it), the VAO
|
// the VAO's config version moving (Enable/DisableAttribute bump it) or the
|
||||||
// changing, or the program's active mask changing, so the per-draw Enabled probes
|
// program's active mask changing, so the per-draw Enabled probes reduce to two
|
||||||
// reduce to three compares. The rebuild visits only the ACTIVE locations - a VAO
|
// compares. The memo lives on the twin (1:1 with the VAO, no identity key
|
||||||
// switch per draw (vanilla cycles section VAOs) pays the same probes the unmemoed
|
// needed): a function-static single entry missed on every draw once the app
|
||||||
// walk did, not a full 32-slot scan. Owner-equality, not the raw pointer: a freed
|
// cycled section VAOs, re-reading the cold attribute slots each time; here a
|
||||||
// VAO's heap slot can be reused, and the held weak_ptr pins the control block so
|
// cycle re-hits every VAO's own entry. The rebuild visits only ACTIVE locations.
|
||||||
// a successor can never alias it.
|
auto& memo = vaoTwin->GetPendingAttribValueMaskMemo();
|
||||||
static WeakPtr<MG_State::GLState::VertexArrayObject> s_pendingMaskVao;
|
|
||||||
static Uint32 s_pendingMaskConfigVersion = 0;
|
|
||||||
static Uint32 s_pendingMaskActiveMask = 0;
|
|
||||||
static Uint32 s_pendingMask = 0;
|
|
||||||
const Uint32 configVersion = vao->GetConfigVersion();
|
const Uint32 configVersion = vao->GetConfigVersion();
|
||||||
if (configVersion != s_pendingMaskConfigVersion || activeAttribMask != s_pendingMaskActiveMask ||
|
if (!memo.valid || configVersion != memo.configVersion || activeAttribMask != memo.activeMask) {
|
||||||
!OwnerEquals(s_pendingMaskVao, vao)) {
|
|
||||||
Uint32 pending = 0;
|
Uint32 pending = 0;
|
||||||
for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) {
|
for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) {
|
||||||
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
||||||
if (!vao->GetAttribute(location).Enabled) pending |= (1u << location);
|
if (!vao->GetAttribute(location).Enabled) pending |= (1u << location);
|
||||||
}
|
}
|
||||||
s_pendingMaskVao = vao;
|
memo.configVersion = configVersion;
|
||||||
s_pendingMaskConfigVersion = configVersion;
|
memo.activeMask = activeAttribMask;
|
||||||
s_pendingMaskActiveMask = activeAttribMask;
|
memo.pendingMask = pending;
|
||||||
s_pendingMask = pending;
|
memo.valid = true;
|
||||||
}
|
}
|
||||||
if (s_pendingMask == 0) return;
|
if (memo.pendingMask == 0) return;
|
||||||
|
|
||||||
for (Uint32 remaining = s_pendingMask; remaining != 0; remaining &= remaining - 1) {
|
for (Uint32 remaining = memo.pendingMask; remaining != 0; remaining &= remaining - 1) {
|
||||||
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
||||||
|
|
||||||
const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);
|
const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);
|
||||||
@@ -1668,6 +1787,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
} // namespace RenderStateImpl
|
} // namespace RenderStateImpl
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
|
// The twin SyncCurrentProgram resolved for the draw/dispatch being prepared,
|
||||||
|
// consumed by BindCurrentProgramWithResources and the post-draw draw-parameter
|
||||||
|
// updates in the same GL entry point — they all used to repeat the registry
|
||||||
|
// Find. Valid only while the keyed frontend program is the current draw
|
||||||
|
// program: every consumer compares the raw key against GetProgramForDraw()
|
||||||
|
// before trusting the twin, and SyncCurrentProgram rewrites the pair at the
|
||||||
|
// top of every PrepareForDraw/PrepareForCompute, so a recycled address can
|
||||||
|
// never be consumed (the stale pair is overwritten before any consumer runs).
|
||||||
|
static const MG_State::GLState::ProgramObject* g_currentDrawFrontendProgram = nullptr;
|
||||||
|
static BackendProgramObjectImpl* g_currentDrawBackendProgram = nullptr;
|
||||||
|
|
||||||
void SyncCurrentProgram() {
|
void SyncCurrentProgram() {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -1675,6 +1805,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_backendProgramObjects.CollectGarbageIfNeeded();
|
g_backendProgramObjects.CollectGarbageIfNeeded();
|
||||||
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
|
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
|
||||||
|
|
||||||
|
g_currentDrawFrontendProgram = nullptr;
|
||||||
|
g_currentDrawBackendProgram = nullptr;
|
||||||
|
|
||||||
auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||||
g_GLESFuncs.glUseProgram(0);
|
g_GLESFuncs.glUseProgram(0);
|
||||||
@@ -1698,24 +1831,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_fragColorBroadcastCount = std::max<Uint>(enabledDrawBuffers, 1);
|
g_fragColorBroadcastCount = std::max<Uint>(enabledDrawBuffers, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* backendProgramSlot = g_backendProgramObjects.Find(currentProgram.get());
|
BackendProgramObjectImpl* twin = g_programTwinLookupMemo.Lookup(currentProgram);
|
||||||
auto& backendObj =
|
if (!twin) {
|
||||||
backendProgramSlot ? *backendProgramSlot : g_backendProgramObjects.GetOrCreate(currentProgram);
|
auto* backendProgramSlot = g_backendProgramObjects.Find(currentProgram.get());
|
||||||
if (!backendObj) {
|
auto& backendObj =
|
||||||
backendObj = MakeShared<BackendProgramObjectImpl>();
|
backendProgramSlot ? *backendProgramSlot : g_backendProgramObjects.GetOrCreate(currentProgram);
|
||||||
backendObj->SyncToBackend(currentProgram);
|
if (!backendObj) {
|
||||||
} else {
|
backendObj = MakeShared<BackendProgramObjectImpl>();
|
||||||
// A link-version mismatch means the program was relinked: the backend
|
|
||||||
// shaders and every cache built by CacheResourceLocations (block
|
|
||||||
// indices, sampler locations, UBO upload gate) are stale.
|
|
||||||
if (!backendObj->GetBackendProgramId() ||
|
|
||||||
backendObj->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
|
|
||||||
backendObj->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
|
||||||
backendObj->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
|
||||||
backendObj->GetFragColorBroadcastCount() != g_fragColorBroadcastCount) {
|
|
||||||
backendObj->SyncToBackend(currentProgram);
|
|
||||||
}
|
}
|
||||||
|
g_programTwinLookupMemo.Store(currentProgram, backendObj.get());
|
||||||
|
twin = backendObj.get();
|
||||||
}
|
}
|
||||||
|
// A link-version mismatch means the program was relinked: the backend
|
||||||
|
// shaders and every cache built by CacheResourceLocations (block
|
||||||
|
// indices, sampler locations, UBO upload gate) are stale.
|
||||||
|
if (!twin->GetBackendProgramId() ||
|
||||||
|
twin->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
|
||||||
|
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
||||||
|
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
||||||
|
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount) {
|
||||||
|
twin->SyncToBackend(currentProgram);
|
||||||
|
}
|
||||||
|
g_currentDrawFrontendProgram = currentProgram.get();
|
||||||
|
g_currentDrawBackendProgram = twin;
|
||||||
}
|
}
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
@@ -1795,8 +1933,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
BufferImpl::SyncNeccessaryBuffers(syncBit & DrawSyncBit::IndexBuffer, syncBit & DrawSyncBit::IndirectBuffer);
|
// One twin resolve serves the whole draw: the buffer sync (which hosts the
|
||||||
VertexArrayImpl::SyncCurrentVAO();
|
// resolved-buffers memo on the twin), the VAO sync and the draw-time bind
|
||||||
|
// below. Nothing in between can invalidate it — the bound VAO is pinned by
|
||||||
|
// the context, and no step here erases or replaces a live VAO's twin.
|
||||||
|
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
VertexArrayImpl::BackendVertexArrayObject* vaoTwin =
|
||||||
|
currentVAO ? VertexArrayImpl::ResolveVaoTwin(currentVAO) : nullptr;
|
||||||
|
|
||||||
|
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, syncBit & DrawSyncBit::IndexBuffer,
|
||||||
|
syncBit & DrawSyncBit::IndirectBuffer);
|
||||||
|
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
|
||||||
TextureImpl::SyncNeccessaryTextures();
|
TextureImpl::SyncNeccessaryTextures();
|
||||||
FramebufferImpl::SyncCurrentFBO();
|
FramebufferImpl::SyncCurrentFBO();
|
||||||
PrgramImpl::SyncCurrentProgram();
|
PrgramImpl::SyncCurrentProgram();
|
||||||
@@ -1808,18 +1955,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedNC("BindCurrentVAO", TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedNC("BindCurrentVAO", TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
if (vaoTwin) {
|
||||||
if (currentVAO) {
|
vaoTwin->Bind();
|
||||||
auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get());
|
|
||||||
if (backendVAOSlot && *backendVAOSlot) {
|
|
||||||
(*backendVAOSlot)->Bind();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
VertexArrayImpl::BindBackendVAOId(0);
|
VertexArrayImpl::BindBackendVAOId(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VertexArrayImpl::SyncCurrentVertexAttributeValues();
|
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin);
|
||||||
|
|
||||||
BindCurrentTextures();
|
BindCurrentTextures();
|
||||||
BindCurrentProgramWithResources();
|
BindCurrentProgramWithResources();
|
||||||
@@ -2125,9 +2268,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get());
|
// The twin SyncCurrentProgram just resolved for this draw; the registry
|
||||||
if (backendProgramSlot && *backendProgramSlot) {
|
// Find only runs if the stash somehow does not match (defensive fallback).
|
||||||
auto& backendProgram = **backendProgramSlot;
|
PrgramImpl::BackendProgramObjectImpl* twin =
|
||||||
|
PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()
|
||||||
|
? PrgramImpl::g_currentDrawBackendProgram
|
||||||
|
: nullptr;
|
||||||
|
if (!twin) {
|
||||||
|
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get());
|
||||||
|
if (backendProgramSlot) {
|
||||||
|
twin = backendProgramSlot->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (twin) {
|
||||||
|
auto& backendProgram = *twin;
|
||||||
backendProgram.Use();
|
backendProgram.Use();
|
||||||
|
|
||||||
// Global UBO: block index and binding-point assignment are cached at
|
// Global UBO: block index and binding-point assignment are cached at
|
||||||
@@ -2305,13 +2459,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static SharedPtr<PrgramImpl::BackendProgramObjectImpl> GetCurrentBackendProgram() {
|
// Raw pointer: only ever used inside one GL entry point after PrepareForDraw/
|
||||||
|
// PrepareForCompute, where the current program (and therefore its registry twin)
|
||||||
|
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
||||||
|
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
|
||||||
|
return PrgramImpl::g_currentDrawBackendProgram;
|
||||||
|
}
|
||||||
if (auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get())) {
|
if (auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get())) {
|
||||||
return *backendProgramSlot;
|
return backendProgramSlot->get();
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -768,6 +768,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return static_cast<GLESBufferResource*>(bufferObject->GetBackendResource().get());
|
return static_cast<GLESBufferResource*>(bufferObject->GetBackendResource().get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource) {
|
||||||
|
// Identity first: a respecify path can hand the frontend a NEW resource; the
|
||||||
|
// memoed pointer is then stale (and only kept alive by the caller's shadow).
|
||||||
|
if (!resource || resource != frontend->GetBackendResource().get()) return false;
|
||||||
|
if (resource->contextGeneration != g_bufferContextGeneration) return false;
|
||||||
|
if (resource->id == 0) return false;
|
||||||
|
// Zero-copy coherent persistent store: EnsureBufferResource's own early-out —
|
||||||
|
// the app writes straight into the mapped GPU storage, nothing to sync.
|
||||||
|
if (resource->persistentMapped) return resource->persistentPtr != nullptr;
|
||||||
|
// A live non-zero-copy map may owe a per-draw SyncPersistentMappedRange push
|
||||||
|
// (persistent maps mutate the shadow without bumping the change serial).
|
||||||
|
if (frontend->IsMapped()) return false;
|
||||||
|
if (resource->pendingRespecify || !resource->storageInitialized) return false;
|
||||||
|
// Same unlocked emptiness probe EnsureBufferResource's replay branch uses.
|
||||||
|
if (!resource->pendingRanges.empty()) return false;
|
||||||
|
if (resource->storageSize != frontend->GetSize()) return false;
|
||||||
|
return resource->syncedChangeSerial.load(std::memory_order_acquire) == frontend->GetChangeSerial();
|
||||||
|
}
|
||||||
|
|
||||||
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
|
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
|||||||
@@ -170,6 +170,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||||
// Existing resource or nullptr; performs no GL calls.
|
// Existing resource or nullptr; performs no GL calls.
|
||||||
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
|
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
|
||||||
|
// True when EnsureBufferResource(frontend) would provably fall straight through
|
||||||
|
// every branch and do no work — i.e. `resource` is still the frontend's own
|
||||||
|
// resource, its id belongs to the live ES context, and either it is the
|
||||||
|
// zero-copy coherent persistent store (draw-time sync is a no-op by design) or
|
||||||
|
// the storage is initialized at the right size with no pending ops and a synced
|
||||||
|
// change serial while the buffer is not mapped (an active map may owe a
|
||||||
|
// per-draw persistent-range push, so it always takes the full path).
|
||||||
|
// `frontend` must be non-null and alive; the caller guarantees that by holding
|
||||||
|
// (or shadowing something that holds) a SharedPtr to it. Enables the per-VAO
|
||||||
|
// resolved-buffers memo to skip EnsureBufferResource on clean static buffers.
|
||||||
|
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource);
|
||||||
|
|
||||||
// Deletes GL buffers whose owning frontend objects died (possibly on a
|
// Deletes GL buffers whose owning frontend objects died (possibly on a
|
||||||
// thread without a current ES context). Called from draw-time sync.
|
// thread without a current ES context). Called from draw-time sync.
|
||||||
@@ -255,7 +266,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
||||||
void Bind() const;
|
void Bind() const;
|
||||||
|
|
||||||
|
// Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the
|
||||||
|
// distinct enabled-attribute buffers (deduped) and the index buffer, resolved
|
||||||
|
// to their backend resources once. Valid while the VAO's config version is
|
||||||
|
// unchanged — every attach/enable/disable/format mutation bumps it (the same
|
||||||
|
// invariant SyncToBackend's gate already leans on), and the VAO's attribute
|
||||||
|
// SharedPtrs pin each memoed frontend buffer for exactly that long, so the raw
|
||||||
|
// pointers cannot dangle on a hit. Per-buffer cleanliness is NOT memoed here:
|
||||||
|
// each hit re-checks IsBufferDrawClean (resource identity, context generation,
|
||||||
|
// pending ops, change serial) and falls back to EnsureBufferResource for just
|
||||||
|
// the dirty entries via their attribute index. The IBO entry is keyed on the
|
||||||
|
// slot's bound-object identity instead (its slot version is a wrapping Uint16
|
||||||
|
// and is not covered by the config version).
|
||||||
|
struct ResolvedDrawBuffers {
|
||||||
|
struct Entry {
|
||||||
|
MG_State::GLState::BufferObject* frontend = nullptr;
|
||||||
|
BufferImpl::GLESBufferResource* resource = nullptr;
|
||||||
|
Uint8 attribIndex = 0;
|
||||||
|
};
|
||||||
|
Bool valid = false;
|
||||||
|
Uint32 configVersion = 0;
|
||||||
|
Uint count = 0;
|
||||||
|
Array<Entry, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> entries;
|
||||||
|
MG_State::GLState::BufferObject* iboFrontend = nullptr;
|
||||||
|
BufferImpl::GLESBufferResource* iboResource = nullptr;
|
||||||
|
};
|
||||||
|
ResolvedDrawBuffers& GetResolvedDrawBuffersMemo() { return m_resolvedDrawBuffers; }
|
||||||
|
|
||||||
|
// Memo for SyncCurrentVertexAttributeValues: which of a program's ACTIVE
|
||||||
|
// attribute locations lack an enabled array in this VAO (those read the
|
||||||
|
// context's current generic value instead of a buffer). Keyed on the VAO
|
||||||
|
// config version (enable/disable bumps it) and the program's active-location
|
||||||
|
// mask. Hosted per twin — the former function-static single entry missed on
|
||||||
|
// every draw once the app cycled VAOs, re-reading the cold attribute slots.
|
||||||
|
struct PendingAttribValueMask {
|
||||||
|
Bool valid = false;
|
||||||
|
Uint32 configVersion = 0;
|
||||||
|
Uint32 activeMask = 0;
|
||||||
|
Uint32 pendingMask = 0;
|
||||||
|
};
|
||||||
|
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||||
|
PendingAttribValueMask m_pendingAttribValueMask;
|
||||||
Uint m_backendVAOId = 0;
|
Uint m_backendVAOId = 0;
|
||||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user