mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
switch (v) {
|
||||
case BlendFactor::Src1Color:
|
||||
@@ -263,12 +323,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// TODO: deletion for deleted objects
|
||||
|
||||
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) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -336,7 +390,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -346,44 +402,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed)
|
||||
// PBO is not needed since it should be handled in frontend
|
||||
|
||||
const auto& currentVAOObject = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!currentVAOObject) {
|
||||
MGLOG_E("No VAO is currently bound, cannot sync necessary buffers.");
|
||||
return;
|
||||
}
|
||||
|
||||
// VBO. Once per distinct buffer, not once per enabled attribute: an interleaved
|
||||
// Minecraft-shaped VAO feeds all of its attributes out of one VBO, so the
|
||||
// per-attribute loop re-ran the whole resource check (context generation, pending
|
||||
// ranges, change serial) 4-8 times over the same object. Repeats were pure
|
||||
// overhead - nothing between two calls can change what the second one would do.
|
||||
const MG_State::GLState::BufferObject*
|
||||
syncedBuffers[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS];
|
||||
Uint syncedBufferCount = 0;
|
||||
for (const auto& attrib : currentVAOObject->GetAllAttributes()) {
|
||||
if (!attrib.Enabled) continue;
|
||||
const auto& bufferObject = attrib.Buffer;
|
||||
if (!bufferObject) continue;
|
||||
|
||||
const auto* bufferKey = bufferObject.get();
|
||||
Bool alreadySynced = false;
|
||||
for (Uint i = 0; i < syncedBufferCount; ++i) {
|
||||
if (syncedBuffers[i] == bufferKey) {
|
||||
alreadySynced = true;
|
||||
break;
|
||||
}
|
||||
// VBO. The distinct-buffer set is memoed on the VAO's backend twin: cycling
|
||||
// hundreds of section VAOs re-walked 32 cold VertexAttribute slots per draw
|
||||
// only to rediscover the same one or two static buffers. While the config
|
||||
// version holds (every attach/enable/disable bumps it, and it pins each
|
||||
// memoed frontend pointer via the attribute SharedPtrs), the walk reduces to
|
||||
// an IsBufferDrawClean probe per distinct buffer; only dirty entries take
|
||||
// EnsureBufferResource, re-fetched through their attribute index.
|
||||
auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr;
|
||||
const Uint32 configVersion = currentVAOObject->GetConfigVersion();
|
||||
if (memo && memo->valid && memo->configVersion == configVersion) {
|
||||
for (Uint i = 0; i < memo->count; ++i) {
|
||||
auto& entry = memo->entries[i];
|
||||
if (IsBufferDrawClean(entry.frontend, entry.resource)) continue;
|
||||
// Same object the entry was built from: config version unchanged.
|
||||
entry.resource = EnsureBufferResource(currentVAOObject->GetAttribute(entry.attribIndex).Buffer);
|
||||
}
|
||||
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;
|
||||
CreateAndSyncBufferObject(bufferObject);
|
||||
auto* const bufferKey = bufferObject.get();
|
||||
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) {
|
||||
auto& possibleIBO = currentVAOObject->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
const auto& possibleIBO = currentVAOObject->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
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;
|
||||
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() {
|
||||
return g_xfbObjects[g_currentXfbName];
|
||||
if (g_currentXfbState == nullptr) {
|
||||
g_currentXfbState = &g_xfbObjects[g_currentXfbName];
|
||||
}
|
||||
return *g_currentXfbState;
|
||||
}
|
||||
|
||||
Bool AreTransformFeedbackObjectsSupported() {
|
||||
@@ -711,6 +816,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void BindTransformFeedback(GLuint name) {
|
||||
g_currentXfbState = nullptr; // name changes; operator[] below may also rehash
|
||||
if (!AreTransformFeedbackObjectsSupported()) {
|
||||
// Without driver objects there is only the default span; keep the frontend
|
||||
// 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) {
|
||||
g_GLESFuncs.glDeleteTransformFeedbacks(1, &it->second.esId);
|
||||
}
|
||||
g_currentXfbState = nullptr; // erase can move values (open addressing)
|
||||
g_xfbObjects.erase(it);
|
||||
// The frontend reverts to the default object when the bound one is deleted.
|
||||
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
|
||||
// driver objects and the frontend objects they pinned all belonged to it.
|
||||
void OnBackendContextDestroyed() {
|
||||
g_currentXfbState = nullptr;
|
||||
g_xfbObjects.clear();
|
||||
g_currentXfbName = 0;
|
||||
g_scatterBufferId = 0;
|
||||
@@ -749,25 +857,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} // namespace XfbImpl
|
||||
|
||||
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
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
g_backendVertexArrayObjects.CollectGarbageIfNeeded();
|
||||
|
||||
auto& currentVAOObject = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!currentVAOObject) {
|
||||
if (!currentVAOObject || !vaoTwin) {
|
||||
MGLOG_E("No VAO is currently bound, cannot sync current VAO.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* backendVAOSlot = g_backendVertexArrayObjects.Find(currentVAOObject.get());
|
||||
auto& backendObj =
|
||||
backendVAOSlot ? *backendVAOSlot : g_backendVertexArrayObjects.GetOrCreate(currentVAOObject);
|
||||
if (!backendObj) {
|
||||
backendObj = MakeShared<VertexArrayImpl::BackendVertexArrayObject>();
|
||||
}
|
||||
backendObj->SyncToBackend(currentVAOObject);
|
||||
vaoTwin->SyncToBackend(currentVAOObject);
|
||||
}
|
||||
|
||||
// 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).
|
||||
// SyncToBackend has already issued glDisableVertexAttribArray for these locations, so the ES
|
||||
// current value is what the shader will actually read.
|
||||
void SyncCurrentVertexAttributeValues() {
|
||||
void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -784,39 +908,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!program) return;
|
||||
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!vao) return;
|
||||
if (!vao || !vaoTwin) return;
|
||||
|
||||
const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask();
|
||||
if (activeAttribMask == 0) return;
|
||||
|
||||
// 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
|
||||
// changing, or the program's active mask changing, so the per-draw Enabled probes
|
||||
// reduce to three compares. The rebuild visits only the ACTIVE locations - a VAO
|
||||
// switch per draw (vanilla cycles section VAOs) pays the same probes the unmemoed
|
||||
// walk did, not a full 32-slot scan. Owner-equality, not the raw pointer: a freed
|
||||
// VAO's heap slot can be reused, and the held weak_ptr pins the control block so
|
||||
// a successor can never alias it.
|
||||
static WeakPtr<MG_State::GLState::VertexArrayObject> s_pendingMaskVao;
|
||||
static Uint32 s_pendingMaskConfigVersion = 0;
|
||||
static Uint32 s_pendingMaskActiveMask = 0;
|
||||
static Uint32 s_pendingMask = 0;
|
||||
// the VAO's config version moving (Enable/DisableAttribute bump it) or the
|
||||
// program's active mask changing, so the per-draw Enabled probes reduce to two
|
||||
// compares. The memo lives on the twin (1:1 with the VAO, no identity key
|
||||
// needed): a function-static single entry missed on every draw once the app
|
||||
// cycled section VAOs, re-reading the cold attribute slots each time; here a
|
||||
// cycle re-hits every VAO's own entry. The rebuild visits only ACTIVE locations.
|
||||
auto& memo = vaoTwin->GetPendingAttribValueMaskMemo();
|
||||
const Uint32 configVersion = vao->GetConfigVersion();
|
||||
if (configVersion != s_pendingMaskConfigVersion || activeAttribMask != s_pendingMaskActiveMask ||
|
||||
!OwnerEquals(s_pendingMaskVao, vao)) {
|
||||
if (!memo.valid || configVersion != memo.configVersion || activeAttribMask != memo.activeMask) {
|
||||
Uint32 pending = 0;
|
||||
for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) {
|
||||
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
||||
if (!vao->GetAttribute(location).Enabled) pending |= (1u << location);
|
||||
}
|
||||
s_pendingMaskVao = vao;
|
||||
s_pendingMaskConfigVersion = configVersion;
|
||||
s_pendingMaskActiveMask = activeAttribMask;
|
||||
s_pendingMask = pending;
|
||||
memo.configVersion = configVersion;
|
||||
memo.activeMask = activeAttribMask;
|
||||
memo.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 auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);
|
||||
@@ -1668,6 +1787,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} // namespace RenderStateImpl
|
||||
|
||||
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() {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -1675,6 +1805,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_backendProgramObjects.CollectGarbageIfNeeded();
|
||||
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
|
||||
|
||||
g_currentDrawFrontendProgram = nullptr;
|
||||
g_currentDrawBackendProgram = nullptr;
|
||||
|
||||
auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
@@ -1698,24 +1831,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_fragColorBroadcastCount = std::max<Uint>(enabledDrawBuffers, 1);
|
||||
}
|
||||
|
||||
auto* backendProgramSlot = g_backendProgramObjects.Find(currentProgram.get());
|
||||
auto& backendObj =
|
||||
backendProgramSlot ? *backendProgramSlot : g_backendProgramObjects.GetOrCreate(currentProgram);
|
||||
if (!backendObj) {
|
||||
backendObj = MakeShared<BackendProgramObjectImpl>();
|
||||
backendObj->SyncToBackend(currentProgram);
|
||||
} else {
|
||||
// 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);
|
||||
BackendProgramObjectImpl* twin = g_programTwinLookupMemo.Lookup(currentProgram);
|
||||
if (!twin) {
|
||||
auto* backendProgramSlot = g_backendProgramObjects.Find(currentProgram.get());
|
||||
auto& backendObj =
|
||||
backendProgramSlot ? *backendProgramSlot : g_backendProgramObjects.GetOrCreate(currentProgram);
|
||||
if (!backendObj) {
|
||||
backendObj = MakeShared<BackendProgramObjectImpl>();
|
||||
}
|
||||
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
|
||||
|
||||
@@ -1795,8 +1933,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
BufferImpl::SyncNeccessaryBuffers(syncBit & DrawSyncBit::IndexBuffer, syncBit & DrawSyncBit::IndirectBuffer);
|
||||
VertexArrayImpl::SyncCurrentVAO();
|
||||
// One twin resolve serves the whole draw: the buffer sync (which hosts the
|
||||
// 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();
|
||||
FramebufferImpl::SyncCurrentFBO();
|
||||
PrgramImpl::SyncCurrentProgram();
|
||||
@@ -1808,18 +1955,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedNC("BindCurrentVAO", TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (currentVAO) {
|
||||
auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get());
|
||||
if (backendVAOSlot && *backendVAOSlot) {
|
||||
(*backendVAOSlot)->Bind();
|
||||
}
|
||||
if (vaoTwin) {
|
||||
vaoTwin->Bind();
|
||||
} else {
|
||||
VertexArrayImpl::BindBackendVAOId(0);
|
||||
}
|
||||
}
|
||||
|
||||
VertexArrayImpl::SyncCurrentVertexAttributeValues();
|
||||
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin);
|
||||
|
||||
BindCurrentTextures();
|
||||
BindCurrentProgramWithResources();
|
||||
@@ -2125,9 +2268,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get());
|
||||
if (backendProgramSlot && *backendProgramSlot) {
|
||||
auto& backendProgram = **backendProgramSlot;
|
||||
// The twin SyncCurrentProgram just resolved for this draw; the registry
|
||||
// Find only runs if the stash somehow does not match (defensive fallback).
|
||||
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();
|
||||
|
||||
// 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();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
|
||||
return PrgramImpl::g_currentDrawBackendProgram;
|
||||
}
|
||||
if (auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get())) {
|
||||
return *backendProgramSlot;
|
||||
return backendProgramSlot->get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -768,6 +768,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
|
||||
@@ -170,6 +170,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||
// Existing resource or nullptr; performs no GL calls.
|
||||
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
|
||||
// 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; }
|
||||
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:
|
||||
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||
PendingAttribValueMask m_pendingAttribValueMask;
|
||||
Uint m_backendVAOId = 0;
|
||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||
Bool m_isInitialized = false;
|
||||
|
||||
Reference in New Issue
Block a user