mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +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).
|
// BufferImpl's context tracking).
|
||||||
Uint g_syncContextGeneration = 1;
|
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
|
// Backend fence handle: a native ES sync plus the ES context
|
||||||
// generation it was created under.
|
// generation it was created under.
|
||||||
struct GLESSyncObject {
|
struct GLESSyncObject {
|
||||||
@@ -4117,8 +4134,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return static_cast<Int64>(timestamp);
|
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() {
|
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() {
|
void DestroyEGLContext() {
|
||||||
@@ -4127,6 +4180,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Outstanding fence handles now refer to a dead context; treat them as
|
// Outstanding fence handles now refer to a dead context; treat them as
|
||||||
// signaled from here on.
|
// signaled from here on.
|
||||||
++g_syncContextGeneration;
|
++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) {
|
if (g_Display != EGL_NO_DISPLAY) {
|
||||||
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
if (g_Context != EGL_NO_CONTEXT) {
|
if (g_Context != EGL_NO_CONTEXT) {
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void DeleteBackendQuery(BackendQueryHandle query);
|
void DeleteBackendQuery(BackendQueryHandle query);
|
||||||
Int64 GetGpuTimestampNs();
|
Int64 GetGpuTimestampNs();
|
||||||
void Present();
|
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
|
// Applies (or defers until the window surface exists) the app-requested
|
||||||
// eglSwapInterval on the native EGL surface.
|
// eglSwapInterval on the native EGL surface.
|
||||||
void SetSwapInterval(Int interval);
|
void SetSwapInterval(Int interval);
|
||||||
|
|||||||
@@ -275,6 +275,84 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
|
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
|
||||||
std::mutex g_deferredBufferReleasesMutex;
|
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) {
|
GLESBufferResource* ResourceOf(BufferObject& bufferObject) {
|
||||||
return static_cast<GLESBufferResource*>(bufferObject.GetBackendResource().get());
|
return static_cast<GLESBufferResource*>(bufferObject.GetBackendResource().get());
|
||||||
}
|
}
|
||||||
@@ -472,6 +550,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (CanTouchGLNow()) {
|
if (CanTouchGLNow()) {
|
||||||
|
if (IsPoolable(*glesResource)) {
|
||||||
|
EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (glesResource->id != 0) {
|
if (glesResource->id != 0) {
|
||||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||||
InvalidateArrayBufferBindingCache();
|
InvalidateArrayBufferBindingCache();
|
||||||
@@ -503,6 +585,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_State::GLState::SetBufferBackendOps(nullptr);
|
MG_State::GLState::SetBufferBackendOps(nullptr);
|
||||||
}
|
}
|
||||||
InvalidateArrayBufferBindingCache();
|
InvalidateArrayBufferBindingCache();
|
||||||
|
// Pooled ids belong to the dying context too; drop them without glDeleteBuffers.
|
||||||
|
ClearBufferPool();
|
||||||
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
|
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
|
||||||
// The ES context owning these ids is going away; just drop the handles.
|
// The ES context owning these ids is going away; just drop the handles.
|
||||||
g_deferredBufferReleases.clear();
|
g_deferredBufferReleases.clear();
|
||||||
@@ -526,6 +610,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
glesResource->id = 0;
|
glesResource->id = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (IsPoolable(*glesResource)) {
|
||||||
|
EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (glesResource->id != 0) {
|
if (glesResource->id != 0) {
|
||||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||||
InvalidateArrayBufferBindingCache();
|
InvalidateArrayBufferBindingCache();
|
||||||
@@ -577,14 +665,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resource->id == 0) {
|
if (resource->id == 0) {
|
||||||
g_GLESFuncs.glGenBuffers(1, &resource->id);
|
// Try to recycle an idle same-size buffer from the pool (GPU-complete,
|
||||||
if (resource->id == 0) {
|
// exact byte size) and reseed it in place with glBufferSubData, instead
|
||||||
MGLOG_E("Failed to generate buffer object.");
|
// of glGenBuffers + fresh-storage glBufferData (the kgsl alloc path).
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
const SizeT poolSize = bufferObject->GetSize();
|
||||||
return resource;
|
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
|
// Push persistently-mapped writes first; lands either as an immediate
|
||||||
@@ -631,6 +741,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_boundArrayBufferId = 0;
|
g_boundArrayBufferId = 0;
|
||||||
g_boundArrayBufferKnown = false;
|
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 BufferImpl
|
||||||
|
|
||||||
namespace VertexArrayImpl {
|
namespace VertexArrayImpl {
|
||||||
|
|||||||
@@ -177,6 +177,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
|
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
|
||||||
void BindBufferId(GLenum target, Uint id);
|
void BindBufferId(GLenum target, Uint id);
|
||||||
void InvalidateArrayBufferBindingCache();
|
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 BufferImpl
|
||||||
|
|
||||||
namespace VertexArrayImpl {
|
namespace VertexArrayImpl {
|
||||||
|
|||||||
Reference in New Issue
Block a user