mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[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:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user