mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Perf] (MG_Backend/DirectGLES): replace per-draw global-UBO glBufferSubData with a persistent-mapped ring allocator (fence-watermark reclaimed, MOBILEGL_DISABLE_UBO_RING opt-out); scrub stale indexed-binding shadow on glDeleteBuffers
This commit is contained in:
@@ -57,6 +57,10 @@ namespace MobileGL::MG_Config {
|
|||||||
Bool CoherentAsFlush = false;
|
Bool CoherentAsFlush = false;
|
||||||
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
||||||
Bool TraceSkipAutodestroy = false;
|
Bool TraceSkipAutodestroy = false;
|
||||||
|
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||||
|
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||||
|
// (negative control / driver-bug escape hatch).
|
||||||
|
Bool DisableUboRing = false;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||||
|
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
@@ -1076,6 +1076,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
const Uint32 uboContentVersion = currentProgram->GetUBOContentVersion();
|
const Uint32 uboContentVersion = currentProgram->GetUBOContentVersion();
|
||||||
|
const SizeT uboSize = static_cast<SizeT>(currentProgram->GetUBOSize());
|
||||||
|
// Preferred path: write changed contents into a fresh slot of the
|
||||||
|
// shared persistent-mapped ring and bind it as a range. The GPU
|
||||||
|
// never reads bytes the CPU is writing, so the driver has no
|
||||||
|
// write-after-read hazard to resolve — the in-place glBufferSubData
|
||||||
|
// below forced Adreno into a ghost/stall on every uniform-dirtying
|
||||||
|
// draw (MC dirties uniforms every draw), which dominated frame time.
|
||||||
|
Bool ringBound = false;
|
||||||
|
if (BufferImpl::UboRingAvailable()) {
|
||||||
|
const SizeT bindSize =
|
||||||
|
std::max(uboSize, static_cast<SizeT>(backendProgram.GetGlobalUboBackendBlockSize()));
|
||||||
|
const Uint64 frameSerial = CurrentFrameSerial();
|
||||||
|
auto& ringSlot = backendProgram.GetGlobalUboRingAllocation();
|
||||||
|
Bool slotValid = ringSlot.ringGeneration == BufferImpl::UboRingGeneration() &&
|
||||||
|
ringSlot.frameSerial == frameSerial &&
|
||||||
|
ringSlot.contentVersion == uboContentVersion;
|
||||||
|
if (!slotValid) {
|
||||||
|
SizeT offset = 0;
|
||||||
|
if (BufferImpl::UboRingAllocate(bindSize, offset)) {
|
||||||
|
std::memcpy(static_cast<Uint8*>(BufferImpl::UboRingMappedPtr()) + offset,
|
||||||
|
currentProgram->MapUBO(), uboSize);
|
||||||
|
ringSlot = {uboContentVersion, BufferImpl::UboRingGeneration(), frameSerial,
|
||||||
|
offset};
|
||||||
|
slotValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slotValid) {
|
||||||
|
BufferImpl::BindBufferRangeCached(GL_UNIFORM_BUFFER, 0, BufferImpl::UboRingBufferId(),
|
||||||
|
static_cast<GLintptr>(ringSlot.offset),
|
||||||
|
static_cast<GLsizeiptr>(bindSize));
|
||||||
|
ringBound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ringBound) {
|
||||||
|
// Fallback (no EXT_buffer_storage / fences, or ring creation
|
||||||
|
// failed): the original in-place upload.
|
||||||
if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) {
|
if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) {
|
||||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
|
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
|
||||||
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(),
|
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(),
|
||||||
@@ -1083,7 +1119,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion);
|
backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion);
|
||||||
}
|
}
|
||||||
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, 0, backendProgram.GetBackendGlobalUBOId());
|
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, 0,
|
||||||
|
backendProgram.GetBackendGlobalUBOId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -4129,6 +4167,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_completedFrameSerial.store(completed, std::memory_order_relaxed);
|
g_completedFrameSerial.store(completed, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After the watermark advanced: retire grown-away UBO-ring stores and record
|
||||||
|
// the frame's ring high-water mark for slot reclamation.
|
||||||
|
BufferImpl::UboRingOnPresent();
|
||||||
BufferImpl::TrimBufferPool();
|
BufferImpl::TrimBufferPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "Managers.h"
|
#include "Managers.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include "DirectGLES.h"
|
#include "DirectGLES.h"
|
||||||
|
#include <Config.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
@@ -270,6 +271,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// an older generation hold ids from a dead context.
|
// an older generation hold ids from a dead context.
|
||||||
Uint g_bufferContextGeneration = 1;
|
Uint g_bufferContextGeneration = 1;
|
||||||
|
|
||||||
|
// Defined next to the indexed-binding shadow below; forward-declared so
|
||||||
|
// every glDeleteBuffers site in this namespace can scrub stale shadow
|
||||||
|
// entries (GL resets a deleted buffer's indexed bindings to 0, and a
|
||||||
|
// recycled name matching a stale shadow entry would otherwise
|
||||||
|
// false-skip the rebind).
|
||||||
|
void ScrubIndexedBufferBindingShadowForId(Uint id);
|
||||||
|
|
||||||
// Resources whose owning BufferObject died; ids deleted at the next
|
// Resources whose owning BufferObject died; ids deleted at the next
|
||||||
// sync point with a current ES context.
|
// sync point with a current ES context.
|
||||||
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
|
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
|
||||||
@@ -313,6 +321,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const std::lock_guard<std::mutex> lock(g_poolMutex);
|
const std::lock_guard<std::mutex> lock(g_poolMutex);
|
||||||
auto& bucket = g_bufferPool[r.storageSize];
|
auto& bucket = g_bufferPool[r.storageSize];
|
||||||
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
|
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
|
||||||
|
ScrubIndexedBufferBindingShadowForId(r.id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool
|
g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool
|
||||||
r.id = 0;
|
r.id = 0;
|
||||||
return;
|
return;
|
||||||
@@ -353,6 +362,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Global-UBO ring (see Managers.h) ------------------------------------
|
||||||
|
constexpr SizeT kUboRingInitialBytes = 4u * 1024u * 1024u;
|
||||||
|
constexpr SizeT kUboRingMaxBytes = 64u * 1024u * 1024u;
|
||||||
|
|
||||||
|
struct UboRingState {
|
||||||
|
Uint id = 0;
|
||||||
|
Uint8* mappedPtr = nullptr;
|
||||||
|
SizeT size = 0;
|
||||||
|
// Monotonic linear cursors: `head` counts every byte ever allocated
|
||||||
|
// (incl. wrap padding); everything below `tail` is GPU-complete. Ring
|
||||||
|
// offset of a linear position is pos % size, so in-flight bytes are
|
||||||
|
// head - tail and must stay <= size.
|
||||||
|
Uint64 head = 0;
|
||||||
|
Uint64 tail = 0;
|
||||||
|
Uint32 generation = 0; // bumped on every (re)create/grow; 0 = never valid
|
||||||
|
Uint contextGeneration = 0;
|
||||||
|
SizeT alignment = 256;
|
||||||
|
// A hard storage-creation failure under this context; stop retrying
|
||||||
|
// per draw (cleared when the context generation moves on).
|
||||||
|
Bool creationFailed = false;
|
||||||
|
};
|
||||||
|
UboRingState g_uboRing;
|
||||||
|
|
||||||
|
// Grown-away ring stores: deletable only once the GPU finished the last
|
||||||
|
// frame that could reference them (same watermark as the buffer pool).
|
||||||
|
struct RetiredUboRing {
|
||||||
|
Uint id = 0;
|
||||||
|
Uint contextGeneration = 0;
|
||||||
|
Uint64 retireSerial = 0;
|
||||||
|
};
|
||||||
|
Vector<RetiredUboRing> g_retiredUboRings;
|
||||||
|
|
||||||
|
// Present()-time high-water marks: every byte below headAtPresent was
|
||||||
|
// written during frames <= frameSerial, so once frameSerial completes,
|
||||||
|
// tail may advance to headAtPresent. FIFO by construction.
|
||||||
|
struct UboRingFrameMark {
|
||||||
|
Uint64 frameSerial = 0;
|
||||||
|
Uint64 headAtPresent = 0;
|
||||||
|
};
|
||||||
|
Vector<UboRingFrameMark> g_uboRingFrameMarks;
|
||||||
|
|
||||||
|
// The ES context the ring's id/map belonged to is gone (or was never
|
||||||
|
// seen): drop every handle without GL calls and re-arm creation.
|
||||||
|
void ResetUboRingForNewContext() {
|
||||||
|
g_uboRing = {};
|
||||||
|
g_uboRing.contextGeneration = g_bufferContextGeneration;
|
||||||
|
g_retiredUboRings.clear();
|
||||||
|
g_uboRingFrameMarks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
GLESBufferResource* ResourceOf(BufferObject& bufferObject) {
|
GLESBufferResource* ResourceOf(BufferObject& bufferObject) {
|
||||||
return static_cast<GLESBufferResource*>(bufferObject.GetBackendResource().get());
|
return static_cast<GLESBufferResource*>(bufferObject.GetBackendResource().get());
|
||||||
}
|
}
|
||||||
@@ -438,6 +497,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Need a fresh id: glBufferStorage fails on a buffer that already has
|
// Need a fresh id: glBufferStorage fails on a buffer that already has
|
||||||
// immutable storage, and any prior mutable store is replaced anyway.
|
// immutable storage, and any prior mutable store is replaced anyway.
|
||||||
if (resource->id != 0) {
|
if (resource->id != 0) {
|
||||||
|
ScrubIndexedBufferBindingShadowForId(resource->id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||||
resource->id = 0;
|
resource->id = 0;
|
||||||
}
|
}
|
||||||
@@ -565,6 +625,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||||
InvalidateArrayBufferBindingCache();
|
InvalidateArrayBufferBindingCache();
|
||||||
}
|
}
|
||||||
|
ScrubIndexedBufferBindingShadowForId(glesResource->id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
||||||
glesResource->id = 0;
|
glesResource->id = 0;
|
||||||
}
|
}
|
||||||
@@ -602,6 +663,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void OnBackendContextDestroyed() {
|
void OnBackendContextDestroyed() {
|
||||||
UnregisterBufferBackendOps();
|
UnregisterBufferBackendOps();
|
||||||
++g_bufferContextGeneration;
|
++g_bufferContextGeneration;
|
||||||
|
// The global-UBO ring's id and persistent map died with the context;
|
||||||
|
// drop the handles (no GL) and let the next draw recreate the ring.
|
||||||
|
ResetUboRingForNewContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProcessDeferredBufferReleases() {
|
void ProcessDeferredBufferReleases() {
|
||||||
@@ -625,6 +689,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||||
InvalidateArrayBufferBindingCache();
|
InvalidateArrayBufferBindingCache();
|
||||||
}
|
}
|
||||||
|
ScrubIndexedBufferBindingShadowForId(glesResource->id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
||||||
glesResource->id = 0;
|
glesResource->id = 0;
|
||||||
}
|
}
|
||||||
@@ -769,6 +834,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index];
|
if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index];
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glDeleteBuffers resets the deleted buffer's bindings (indexed ones
|
||||||
|
// included) to 0 in the current context; mirror that in the shadow, or a
|
||||||
|
// later buffer recycling the same name with a matching range would
|
||||||
|
// false-skip its rebind. Default IndexedBufferBinding{} == base(0) ==
|
||||||
|
// the post-delete GL state.
|
||||||
|
void ScrubIndexedBufferBindingShadowForId(Uint id) {
|
||||||
|
if (id == 0) return;
|
||||||
|
for (auto& binding : g_indexedUBOBindings) {
|
||||||
|
if (binding.id == id) binding = {};
|
||||||
|
}
|
||||||
|
for (auto& binding : g_indexedSSBOBindings) {
|
||||||
|
if (binding.id == id) binding = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) {
|
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) {
|
||||||
@@ -812,6 +892,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
auto& bucket = g_bufferPool[oldestKey];
|
auto& bucket = g_bufferPool[oldestKey];
|
||||||
PooledBuffer& e = bucket[oldestIdx];
|
PooledBuffer& e = bucket[oldestIdx];
|
||||||
if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) {
|
if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) {
|
||||||
|
ScrubIndexedBufferBindingShadowForId(e.id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &e.id);
|
g_GLESFuncs.glDeleteBuffers(1, &e.id);
|
||||||
}
|
}
|
||||||
g_pooledBytes -= e.size;
|
g_pooledBytes -= e.size;
|
||||||
@@ -827,6 +908,167 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_bufferPool.clear();
|
g_bufferPool.clear();
|
||||||
g_pooledBytes = 0;
|
g_pooledBytes = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Global-UBO ring (see Managers.h) ------------------------------------
|
||||||
|
namespace {
|
||||||
|
// (Re)create the ring store with room for at least minBytes. Any live
|
||||||
|
// store is retired (deleted once the GPU finished the last frame that
|
||||||
|
// could reference its slots), never deleted in place. Returns false and
|
||||||
|
// leaves the current store untouched when minBytes cannot fit under the
|
||||||
|
// size cap; a GL failure loses the store and latches creationFailed so
|
||||||
|
// draws stop retrying under this context.
|
||||||
|
Bool CreateUboRingStorage(SizeT minBytes) {
|
||||||
|
SizeT newSize = kUboRingInitialBytes;
|
||||||
|
while (newSize < minBytes) newSize *= 2;
|
||||||
|
if (newSize > kUboRingMaxBytes) return false;
|
||||||
|
|
||||||
|
if (g_uboRing.id != 0) {
|
||||||
|
g_retiredUboRings.push_back(
|
||||||
|
{g_uboRing.id, g_uboRing.contextGeneration, DirectGLES::CurrentFrameSerial() + 1});
|
||||||
|
}
|
||||||
|
const Uint32 nextGeneration = g_uboRing.generation + 1;
|
||||||
|
g_uboRing.id = 0;
|
||||||
|
g_uboRing.mappedPtr = nullptr;
|
||||||
|
|
||||||
|
Uint id = 0;
|
||||||
|
g_GLESFuncs.glGenBuffers(1, &id);
|
||||||
|
if (id != 0) {
|
||||||
|
BindBufferId(TempBufferTarget, id);
|
||||||
|
g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast<GLsizeiptr>(newSize), nullptr,
|
||||||
|
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
|
||||||
|
void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(newSize),
|
||||||
|
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
|
||||||
|
if (!ptr) {
|
||||||
|
g_GLESFuncs.glDeleteBuffers(1, &id);
|
||||||
|
id = 0;
|
||||||
|
} else {
|
||||||
|
g_uboRing.mappedPtr = static_cast<Uint8*>(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (id == 0) {
|
||||||
|
MGLOG_E("Global-UBO ring: persistent storage creation failed (%zu bytes); "
|
||||||
|
"falling back to glBufferSubData uploads.",
|
||||||
|
newSize);
|
||||||
|
g_uboRing.creationFailed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLint capsAlignment = g_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||||
|
SizeT alignment = capsAlignment > 0 ? static_cast<SizeT>(capsAlignment) : 256;
|
||||||
|
// The cursor math masks with (alignment - 1); spec doesn't promise a
|
||||||
|
// power of two, so round up to one.
|
||||||
|
SizeT pow2 = 1;
|
||||||
|
while (pow2 < alignment) pow2 <<= 1;
|
||||||
|
g_uboRing.id = id;
|
||||||
|
g_uboRing.size = newSize;
|
||||||
|
g_uboRing.head = 0;
|
||||||
|
g_uboRing.tail = 0;
|
||||||
|
g_uboRing.generation = nextGeneration;
|
||||||
|
g_uboRing.alignment = pow2;
|
||||||
|
g_uboRingFrameMarks.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool UboRingAvailable() {
|
||||||
|
if (MG_Config::Features.DisableUboRing) return false;
|
||||||
|
// Reclamation rides the Present fence watermark; without working fences
|
||||||
|
// slots would never be provably GPU-idle (same rule as IsPoolable).
|
||||||
|
if (!g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers ||
|
||||||
|
!g_GLESFuncs.glFenceSync || !g_GLESFuncs.glGetSynciv) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!CanTouchGLNow()) return false;
|
||||||
|
if (g_uboRing.contextGeneration != g_bufferContextGeneration) {
|
||||||
|
ResetUboRingForNewContext();
|
||||||
|
}
|
||||||
|
return !g_uboRing.creationFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool UboRingAllocate(SizeT size, SizeT& outOffset) {
|
||||||
|
if (size == 0 || !UboRingAvailable()) return false;
|
||||||
|
const SizeT alignedSize = (size + g_uboRing.alignment - 1) & ~(g_uboRing.alignment - 1);
|
||||||
|
if (g_uboRing.id == 0 && !CreateUboRingStorage(alignedSize)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance tail past every frame the GPU provably finished.
|
||||||
|
const Uint64 completed = DirectGLES::CompletedFrameSerial();
|
||||||
|
SizeT retiredMarks = 0;
|
||||||
|
for (const auto& mark : g_uboRingFrameMarks) {
|
||||||
|
if (mark.frameSerial > completed) break;
|
||||||
|
if (mark.headAtPresent > g_uboRing.tail) g_uboRing.tail = mark.headAtPresent;
|
||||||
|
++retiredMarks;
|
||||||
|
}
|
||||||
|
if (retiredMarks > 0) {
|
||||||
|
g_uboRingFrameMarks.erase(g_uboRingFrameMarks.begin(),
|
||||||
|
g_uboRingFrameMarks.begin() + static_cast<std::ptrdiff_t>(retiredMarks));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A slot may not straddle the ring end; pad the cursor to the boundary.
|
||||||
|
SizeT offset = static_cast<SizeT>(g_uboRing.head % g_uboRing.size);
|
||||||
|
if (offset + alignedSize > g_uboRing.size) {
|
||||||
|
g_uboRing.head += g_uboRing.size - offset;
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_uboRing.head + alignedSize - g_uboRing.tail > g_uboRing.size) {
|
||||||
|
// In-flight span would overrun live slots: grow instead of overwrite.
|
||||||
|
if (CreateUboRingStorage(std::max(g_uboRing.size * 2, alignedSize))) {
|
||||||
|
offset = 0;
|
||||||
|
} else if (g_uboRing.creationFailed) {
|
||||||
|
return false; // store lost; callers fall back to glBufferSubData
|
||||||
|
} else {
|
||||||
|
// At the size cap (>kUboRingMaxBytes of uniforms in flight — not a
|
||||||
|
// real workload): drain the GPU once rather than corrupt live slots.
|
||||||
|
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
||||||
|
g_uboRing.tail = g_uboRing.head;
|
||||||
|
g_uboRingFrameMarks.clear();
|
||||||
|
offset = static_cast<SizeT>(g_uboRing.head % g_uboRing.size);
|
||||||
|
if (offset + alignedSize > g_uboRing.size) {
|
||||||
|
g_uboRing.head += g_uboRing.size - offset;
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_uboRing.head += alignedSize;
|
||||||
|
outOffset = offset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* UboRingMappedPtr() { return g_uboRing.mappedPtr; }
|
||||||
|
Uint UboRingBufferId() { return g_uboRing.id; }
|
||||||
|
Uint32 UboRingGeneration() { return g_uboRing.generation; }
|
||||||
|
|
||||||
|
void UboRingOnPresent() {
|
||||||
|
if (!CanTouchGLNow()) return;
|
||||||
|
|
||||||
|
// Delete grown-away stores the GPU is provably done with.
|
||||||
|
const Uint64 completed = DirectGLES::CompletedFrameSerial();
|
||||||
|
for (SizeT i = g_retiredUboRings.size(); i-- > 0;) {
|
||||||
|
RetiredUboRing& entry = g_retiredUboRings[i];
|
||||||
|
const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration;
|
||||||
|
if (!staleContext && entry.retireSerial > completed) continue;
|
||||||
|
if (!staleContext && entry.id != 0) {
|
||||||
|
ScrubIndexedBufferBindingShadowForId(entry.id);
|
||||||
|
g_GLESFuncs.glDeleteBuffers(1, &entry.id);
|
||||||
|
}
|
||||||
|
g_retiredUboRings[i] = g_retiredUboRings.back();
|
||||||
|
g_retiredUboRings.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_uboRing.id == 0 || g_uboRing.contextGeneration != g_bufferContextGeneration) return;
|
||||||
|
// Record this frame's high-water mark (Present just fenced the serial now
|
||||||
|
// reported by CurrentFrameSerial()). A fence-less Present repeats the
|
||||||
|
// serial; fold into the existing mark.
|
||||||
|
const Uint64 serial = DirectGLES::CurrentFrameSerial();
|
||||||
|
if (!g_uboRingFrameMarks.empty() && g_uboRingFrameMarks.back().frameSerial == serial) {
|
||||||
|
g_uboRingFrameMarks.back().headAtPresent = g_uboRing.head;
|
||||||
|
} else {
|
||||||
|
g_uboRingFrameMarks.push_back({serial, g_uboRing.head});
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace BufferImpl
|
} // namespace BufferImpl
|
||||||
|
|
||||||
namespace VertexArrayImpl {
|
namespace VertexArrayImpl {
|
||||||
@@ -2576,13 +2818,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BackendProgramObjectImpl::CacheResourceLocations(
|
void BackendProgramObjectImpl::CacheResourceLocations(
|
||||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||||
m_globalUboBackendBlockIndex = -1;
|
m_globalUboBackendBlockIndex = -1;
|
||||||
|
m_globalUboBackendBlockSize = 0;
|
||||||
m_lastUploadedGlobalUboVersion = ~0u;
|
m_lastUploadedGlobalUboVersion = ~0u;
|
||||||
|
m_globalUboRingAllocation = {};
|
||||||
if (stateProgramObject->GetUBOSize() > 0) {
|
if (stateProgramObject->GetUBOSize() > 0) {
|
||||||
const Uint blockIndex =
|
const Uint blockIndex =
|
||||||
g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
||||||
if (blockIndex != GL_INVALID_INDEX) {
|
if (blockIndex != GL_INVALID_INDEX) {
|
||||||
m_globalUboBackendBlockIndex = static_cast<Int>(blockIndex);
|
m_globalUboBackendBlockIndex = static_cast<Int>(blockIndex);
|
||||||
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, blockIndex, 0);
|
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, blockIndex, 0);
|
||||||
|
// Ring bindings are ranges and must span the block as the backend
|
||||||
|
// compiled it (its std140 padding may exceed the frontend's
|
||||||
|
// SPIR-V-reflected size).
|
||||||
|
if (g_GLESFuncs.glGetActiveUniformBlockiv) {
|
||||||
|
GLint blockDataSize = 0;
|
||||||
|
g_GLESFuncs.glGetActiveUniformBlockiv(m_backendProgramId, blockIndex,
|
||||||
|
GL_UNIFORM_BLOCK_DATA_SIZE, &blockDataSize);
|
||||||
|
m_globalUboBackendBlockSize = static_cast<Int>(blockDataSize);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
MGLOG_W("Program %u has frontend global UBO storage, but backend has no %s block.",
|
MGLOG_W("Program %u has frontend global UBO storage, but backend has no %s block.",
|
||||||
stateProgramObject->GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
stateProgramObject->GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
||||||
|
|||||||
@@ -189,6 +189,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// without glDeleteBuffers (called when the ES context is going away).
|
// without glDeleteBuffers (called when the ES context is going away).
|
||||||
void TrimBufferPool();
|
void TrimBufferPool();
|
||||||
void ClearBufferPool();
|
void ClearBufferPool();
|
||||||
|
|
||||||
|
// --- Global-UBO ring ------------------------------------------------------
|
||||||
|
// One persistently+coherently mapped buffer (EXT_buffer_storage) shared by
|
||||||
|
// every program's lowered default-uniform block. Each content change is
|
||||||
|
// bump-allocated into a fresh slot and bound with glBindBufferRange, so the
|
||||||
|
// CPU never rewrites bytes the GPU may still be reading — the per-draw
|
||||||
|
// glBufferSubData into one static UBO forced Adreno to resolve that
|
||||||
|
// write-after-read hazard on every uniform-dirtying draw (MC dirties
|
||||||
|
// uniforms every draw). Reclamation rides the Present() frame-fence
|
||||||
|
// watermark; no ring bytes are recycled before their frame's GPU work
|
||||||
|
// completed.
|
||||||
|
//
|
||||||
|
// A program's cached slot, reusable within one frame while the frontend UBO
|
||||||
|
// content version is unchanged. Cross-frame reuse is intentionally not
|
||||||
|
// attempted: later same-frame allocations may recycle bytes of completed
|
||||||
|
// frames, so re-referencing them would need per-bind pinning — rewriting
|
||||||
|
// GetUBOSize() bytes once per program per frame is far cheaper.
|
||||||
|
struct UboRingAllocation {
|
||||||
|
Uint32 contentVersion = ~0u; // frontend UBO content version held at `offset`
|
||||||
|
Uint32 ringGeneration = 0; // ring identity the slot lives in (0 = never valid)
|
||||||
|
Uint64 frameSerial = ~Uint64{0}; // frame the slot was written in
|
||||||
|
SizeT offset = 0;
|
||||||
|
};
|
||||||
|
// False when the feature is disabled, EXT_buffer_storage / fences are
|
||||||
|
// missing, the ES context is not current, or ring creation already failed
|
||||||
|
// under this context (callers then take the legacy glBufferSubData path).
|
||||||
|
Bool UboRingAvailable();
|
||||||
|
// Bump-allocate `size` bytes aligned to GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT.
|
||||||
|
// Grows the ring (new GL store, generation bump) when the in-flight span
|
||||||
|
// would be overrun. Returns false when storage (re)creation fails.
|
||||||
|
Bool UboRingAllocate(SizeT size, SizeT& outOffset);
|
||||||
|
void* UboRingMappedPtr();
|
||||||
|
Uint UboRingBufferId();
|
||||||
|
Uint32 UboRingGeneration();
|
||||||
|
// Present()-time upkeep: records the frame's high-water mark for reclamation
|
||||||
|
// and deletes grown-away ring stores once the GPU is done with them.
|
||||||
|
void UboRingOnPresent();
|
||||||
} // namespace BufferImpl
|
} // namespace BufferImpl
|
||||||
|
|
||||||
namespace VertexArrayImpl {
|
namespace VertexArrayImpl {
|
||||||
@@ -392,6 +429,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Vector<SamplerUniformBinding>& GetSamplerUniformBindings() { return m_samplerUniformBindings; }
|
Vector<SamplerUniformBinding>& GetSamplerUniformBindings() { return m_samplerUniformBindings; }
|
||||||
Uint32 GetLastUploadedGlobalUboVersion() const { return m_lastUploadedGlobalUboVersion; }
|
Uint32 GetLastUploadedGlobalUboVersion() const { return m_lastUploadedGlobalUboVersion; }
|
||||||
void SetLastUploadedGlobalUboVersion(Uint32 version) { m_lastUploadedGlobalUboVersion = version; }
|
void SetLastUploadedGlobalUboVersion(Uint32 version) { m_lastUploadedGlobalUboVersion = version; }
|
||||||
|
// Backend-reported GL_UNIFORM_BLOCK_DATA_SIZE of the global block; ring
|
||||||
|
// bindings must span at least this much (may exceed the frontend's
|
||||||
|
// reflected size when the transpiled block pads differently).
|
||||||
|
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
|
||||||
|
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
|
||||||
// Frontend link version this backend program (and its resource caches) was
|
// Frontend link version this backend program (and its resource caches) was
|
||||||
// built from; a mismatch means every link-derived cache here is stale.
|
// built from; a mismatch means every link-derived cache here is stale.
|
||||||
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
||||||
@@ -410,9 +452,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
|
|
||||||
Int m_globalUboBackendBlockIndex = -1;
|
Int m_globalUboBackendBlockIndex = -1;
|
||||||
|
Int m_globalUboBackendBlockSize = 0;
|
||||||
Vector<Int> m_uniformBlockBackendIndices; // frontend block index -> backend index (-1 = absent)
|
Vector<Int> m_uniformBlockBackendIndices; // frontend block index -> backend index (-1 = absent)
|
||||||
Vector<SamplerUniformBinding> m_samplerUniformBindings;
|
Vector<SamplerUniformBinding> m_samplerUniformBindings;
|
||||||
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
||||||
|
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||||
Uint32 m_syncedLinkVersion = ~0u;
|
Uint32 m_syncedLinkVersion = ~0u;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user