[Perf] (MG_Backend/DirectGLES): recycle idle GL buffers via a fence-gated size pool instead of glDeleteBuffers; pinned fps 184->220

This commit is contained in:
2026-07-12 05:28:34 -04:00
parent 340449b77e
commit d7029952bb
4 changed files with 226 additions and 8 deletions
+60 -1
View File
@@ -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<Uint64> g_currentFrameSerial{0};
std::atomic<Uint64> 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<Int64>(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) {
@@ -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);
+155 -7
View File
@@ -275,6 +275,84 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<SharedPtr<BackendBufferResource>> 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<SizeT, Vector<PooledBuffer>> 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<std::mutex> 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<std::mutex> 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<GLESBufferResource*>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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 {
@@ -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 {