diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 2780dcc5..3dbf0788 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3818,6 +3818,23 @@ namespace MobileGL::MG_Backend::DirectGLES { // BufferImpl's context tracking). Uint g_syncContextGeneration = 1; + // One-fence-per-frame ring driving the buffer-storage pool's recycle gate. + // A buffer retired during frame N is safe to reuse once frame N's fence has + // signaled. Touched only in Present()/DestroyEGLContext() on the owning + // thread -> no lock. Each fence carries the sync generation so a dead-context + // GLsync is never polled/deleted. Depth 4 >> the 2-3 frames Adreno keeps in + // flight; wrap-before-signal only happens during a stall and just degrades to + // the allocate path. + std::atomic g_currentFrameSerial{0}; + std::atomic g_completedFrameSerial{0}; + constexpr int kFrameFenceRingDepth = 4; + struct FrameFence { + GLsync sync = nullptr; + Uint contextGeneration = 0; + Uint64 serial = 0; + }; + FrameFence g_frameFenceRing[kFrameFenceRingDepth]; + // Backend fence handle: a native ES sync plus the ES context // generation it was created under. struct GLESSyncObject { @@ -4117,8 +4134,44 @@ namespace MobileGL::MG_Backend::DirectGLES { return static_cast(timestamp); } + Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); } + Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); } + void Present() { - g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface); + // Insert one fence per frame BEFORE the swap (eglSwapBuffers' implicit flush + // makes it reachable), then non-blocking-poll prior frames' fences AFTER to + // advance the completed-frame watermark that gates buffer-pool recycling. + const Bool canFence = IsBackendContextCurrentOnThisThread() && g_GLESFuncs.glFenceSync; + if (canFence) { + const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1; + FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth]; + if (slot.sync && slot.contextGeneration == g_syncContextGeneration && g_GLESFuncs.glDeleteSync) { + g_GLESFuncs.glDeleteSync(slot.sync); + } + slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial}; + } + + g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface); + + if (canFence && g_GLESFuncs.glGetSynciv) { + // Fences signal in submission order within one context, so the highest + // signaled serial is a valid contiguous completion watermark. + Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed); + for (FrameFence& slot : g_frameFenceRing) { + if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue; + GLint status = GL_SIGNALED; + GLsizei length = 0; + g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status); + if (status == GL_SIGNALED) { + if (slot.serial > completed) completed = slot.serial; + if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync); + slot.sync = nullptr; + } + } + g_completedFrameSerial.store(completed, std::memory_order_relaxed); + } + + BufferImpl::TrimBufferPool(); } void DestroyEGLContext() { @@ -4127,6 +4180,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // Outstanding fence handles now refer to a dead context; treat them as // signaled from here on. ++g_syncContextGeneration; + // The frame-fence ring's syncs belong to the dead context too; abandon them + // (the context reclaims its syncs) and floor the completed watermark to the + // current serial so buffers retired under the old context read as GPU-idle. + for (FrameFence& slot : g_frameFenceRing) slot = {}; + g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed), + std::memory_order_relaxed); if (g_Display != EGL_NO_DISPLAY) { g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (g_Context != EGL_NO_CONTEXT) { diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 7495f20d..052eceb9 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -136,6 +136,12 @@ namespace MobileGL::MG_Backend::DirectGLES { void DeleteBackendQuery(BackendQueryHandle query); Int64 GetGpuTimestampNs(); void Present(); + // Frame-completion watermarks for the buffer-storage pool: CurrentFrameSerial() + // is bumped once per Present(); CompletedFrameSerial() is the newest frame whose + // GPU work has provably finished (advanced by polling a one-fence-per-frame ring). + // A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N. + Uint64 CurrentFrameSerial(); + Uint64 CompletedFrameSerial(); // Applies (or defers until the window surface exists) the app-requested // eglSwapInterval on the native EGL surface. void SetSwapInterval(Int interval); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 531ee101..109dcf2a 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -275,6 +275,84 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector> g_deferredBufferReleases; std::mutex g_deferredBufferReleasesMutex; + // --- Buffer-storage pool (Mesa-style BO recycle) ------------------------- + // Recycle idle GL buffer ids of an EXACT byte size instead of glDeleteBuffers + // (which triggers the kgsl_sharedmem_free -> mmu_unmap -> smmu/power/bandwidth + // cascade that dominated per-frame driver cost). An id retired during frame N + // is handed back only once the GPU has completed frame N (fence watermark, see + // DirectGLES::CompletedFrameSerial), then reseeded in place with glBufferSubData + // (no glBufferData realloc). All GL access is on the ES-context-owning thread; + // the mutex only guards against off-thread deferred-release enrollment races. + struct PooledBuffer { + Uint id = 0; + SizeT size = 0; + Uint contextGeneration = 0; + Uint64 retireSerial = 0; + }; + UnorderedMap> g_bufferPool; + SizeT g_pooledBytes = 0; + std::mutex g_poolMutex; + constexpr SizeT kMaxPoolableBufferBytes = 8u * 1024u * 1024u; // bigger buffers: delete now + constexpr SizeT kMaxPoolBytes = 64u * 1024u * 1024u; // total pool budget + constexpr SizeT kMaxEntriesPerBucket = 32; + + Bool IsPoolable(const GLESBufferResource& r) { + // Require working fences: recycling is gated on the frame-completion + // watermark, which only advances if Present can insert/poll fences. + return g_GLESFuncs.glFenceSync != nullptr && g_GLESFuncs.glGetSynciv != nullptr && + r.id != 0 && !r.persistentMapped && r.contextGeneration == g_bufferContextGeneration && + r.storageInitialized && r.storageSize > 0 && r.storageSize <= kMaxPoolableBufferBytes; + } + + // Retire a buffer id into the pool (owning thread; caller verified IsPoolable). + // Zeroes r.id to keep the single-owner invariant {live | deferred | pool}. + void EnrollIntoPool(GLESBufferResource& r) { + if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) { + InvalidateArrayBufferBindingCache(); + } + const std::lock_guard lock(g_poolMutex); + auto& bucket = g_bufferPool[r.storageSize]; + if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) { + g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool + r.id = 0; + return; + } + // +1: Present increments the serial at frame END, so during the frame + // now being built CurrentFrameSerial() reads (frame-1). A buffer used + // this frame is only GPU-done once THIS frame's fence (serial+1) signals. + bucket.push_back( + {r.id, r.storageSize, r.contextGeneration, DirectGLES::CurrentFrameSerial() + 1}); + g_pooledBytes += r.storageSize; + r.id = 0; + } + + // Hand back an idle pooled id of EXACTLY `size` whose GPU work is complete, + // else 0. Owning thread only. Drops stale-generation entries encountered. + Uint AcquireFromPool(SizeT size) { + const Uint64 completed = DirectGLES::CompletedFrameSerial(); + const std::lock_guard lock(g_poolMutex); + auto it = g_bufferPool.find(size); + if (it == g_bufferPool.end()) return 0; + auto& bucket = it->second; + for (SizeT i = bucket.size(); i-- > 0;) { // newest-first: hottest + most-likely-idle + PooledBuffer& e = bucket[i]; + if (e.contextGeneration != g_bufferContextGeneration) { + g_pooledBytes -= e.size; // dead-context id: drop, no GL + bucket[i] = bucket.back(); + bucket.pop_back(); + continue; + } + if (e.retireSerial <= completed) { + const Uint id = e.id; + g_pooledBytes -= e.size; + bucket[i] = bucket.back(); + bucket.pop_back(); + return id; + } + } + return 0; + } + GLESBufferResource* ResourceOf(BufferObject& bufferObject) { return static_cast(bufferObject.GetBackendResource().get()); } @@ -472,6 +550,10 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } if (CanTouchGLNow()) { + if (IsPoolable(*glesResource)) { + EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers + return; + } if (glesResource->id != 0) { if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { InvalidateArrayBufferBindingCache(); @@ -503,6 +585,8 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_State::GLState::SetBufferBackendOps(nullptr); } InvalidateArrayBufferBindingCache(); + // Pooled ids belong to the dying context too; drop them without glDeleteBuffers. + ClearBufferPool(); const std::lock_guard lock(g_deferredBufferReleasesMutex); // The ES context owning these ids is going away; just drop the handles. g_deferredBufferReleases.clear(); @@ -526,6 +610,10 @@ namespace MobileGL::MG_Backend::DirectGLES { glesResource->id = 0; continue; } + if (IsPoolable(*glesResource)) { + EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers + continue; + } if (glesResource->id != 0) { if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { InvalidateArrayBufferBindingCache(); @@ -577,14 +665,36 @@ namespace MobileGL::MG_Backend::DirectGLES { } if (resource->id == 0) { - g_GLESFuncs.glGenBuffers(1, &resource->id); - if (resource->id == 0) { - MGLOG_E("Failed to generate buffer object."); - MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - return resource; + // Try to recycle an idle same-size buffer from the pool (GPU-complete, + // exact byte size) and reseed it in place with glBufferSubData, instead + // of glGenBuffers + fresh-storage glBufferData (the kgsl alloc path). + const SizeT poolSize = bufferObject->GetSize(); + const Uint reused = + (poolSize > 0 && !resource->persistentMapped) ? AcquireFromPool(poolSize) : 0; + if (reused != 0) { + resource->id = reused; + resource->storageSize = poolSize; + resource->storageInitialized = true; + resource->pendingRespecify = false; + BindBufferId(TempBufferTarget, reused); + g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize, + bufferObject->MappedData()); + { + const std::lock_guard lock(resource->pendingMutex); + resource->pendingRanges.clear(); + } + resource->syncedChangeSerial = bufferObject->GetChangeSerial(); + } else { + g_GLESFuncs.glGenBuffers(1, &resource->id); + if (resource->id == 0) { + MGLOG_E("Failed to generate buffer object."); + MGLOG_E("ES glGetError(): %s", + MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + return resource; + } + resource->storageInitialized = false; + resource->pendingRespecify = true; } - resource->storageInitialized = false; - resource->pendingRespecify = true; } // Push persistently-mapped writes first; lands either as an immediate @@ -631,6 +741,44 @@ namespace MobileGL::MG_Backend::DirectGLES { g_boundArrayBufferId = 0; g_boundArrayBufferKnown = false; } + + void TrimBufferPool() { + const std::lock_guard lock(g_poolMutex); + if (g_pooledBytes <= kMaxPoolBytes) return; + // Over budget: evict oldest-retireSerial entries with real glDeleteBuffers. + while (g_pooledBytes > kMaxPoolBytes) { + SizeT oldestKey = 0, oldestIdx = 0; + Uint64 oldestSerial = ~Uint64{0}; + Bool found = false; + for (auto& kv : g_bufferPool) { + for (SizeT i = 0; i < kv.second.size(); ++i) { + if (kv.second[i].retireSerial < oldestSerial) { + oldestSerial = kv.second[i].retireSerial; + oldestKey = kv.first; + oldestIdx = i; + found = true; + } + } + } + if (!found) break; + auto& bucket = g_bufferPool[oldestKey]; + PooledBuffer& e = bucket[oldestIdx]; + if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) { + g_GLESFuncs.glDeleteBuffers(1, &e.id); + } + g_pooledBytes -= e.size; + bucket[oldestIdx] = bucket.back(); + bucket.pop_back(); + } + } + + void ClearBufferPool() { + const std::lock_guard lock(g_poolMutex); + // Ids belong to the dying context; drop without glDeleteBuffers (mirrors + // the g_deferredBufferReleases.clear() discipline). + g_bufferPool.clear(); + g_pooledBytes = 0; + } } // namespace BufferImpl namespace VertexArrayImpl { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index bb949a32..e0607563 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -177,6 +177,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER. void BindBufferId(GLenum target, Uint id); void InvalidateArrayBufferBindingCache(); + // Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries + // (called once per frame from Present); ClearBufferPool drops all pooled ids + // without glDeleteBuffers (called when the ES context is going away). + void TrimBufferPool(); + void ClearBufferPool(); } // namespace BufferImpl namespace VertexArrayImpl {