mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 12:18: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;
|
||||
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
||||
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;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -121,6 +121,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -1076,14 +1076,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
const Uint32 uboContentVersion = currentProgram->GetUBOContentVersion();
|
||||
if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) {
|
||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
|
||||
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(),
|
||||
currentProgram->MapUBO());
|
||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion);
|
||||
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) {
|
||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
|
||||
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(),
|
||||
currentProgram->MapUBO());
|
||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Managers.h"
|
||||
#include "Utils.h"
|
||||
#include "DirectGLES.h"
|
||||
#include <Config.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.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.
|
||||
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
|
||||
// sync point with a current ES context.
|
||||
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
|
||||
@@ -313,6 +321,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const std::lock_guard<std::mutex> lock(g_poolMutex);
|
||||
auto& bucket = g_bufferPool[r.storageSize];
|
||||
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
|
||||
ScrubIndexedBufferBindingShadowForId(r.id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool
|
||||
r.id = 0;
|
||||
return;
|
||||
@@ -353,6 +362,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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) {
|
||||
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
|
||||
// immutable storage, and any prior mutable store is replaced anyway.
|
||||
if (resource->id != 0) {
|
||||
ScrubIndexedBufferBindingShadowForId(resource->id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||
resource->id = 0;
|
||||
}
|
||||
@@ -565,6 +625,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||
InvalidateArrayBufferBindingCache();
|
||||
}
|
||||
ScrubIndexedBufferBindingShadowForId(glesResource->id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
||||
glesResource->id = 0;
|
||||
}
|
||||
@@ -602,6 +663,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void OnBackendContextDestroyed() {
|
||||
UnregisterBufferBackendOps();
|
||||
++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() {
|
||||
@@ -625,6 +689,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||
InvalidateArrayBufferBindingCache();
|
||||
}
|
||||
ScrubIndexedBufferBindingShadowForId(glesResource->id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
||||
glesResource->id = 0;
|
||||
}
|
||||
@@ -769,6 +834,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index];
|
||||
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
|
||||
|
||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) {
|
||||
@@ -812,6 +892,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto& bucket = g_bufferPool[oldestKey];
|
||||
PooledBuffer& e = bucket[oldestIdx];
|
||||
if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) {
|
||||
ScrubIndexedBufferBindingShadowForId(e.id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &e.id);
|
||||
}
|
||||
g_pooledBytes -= e.size;
|
||||
@@ -827,6 +908,167 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_bufferPool.clear();
|
||||
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 VertexArrayImpl {
|
||||
@@ -2576,13 +2818,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BackendProgramObjectImpl::CacheResourceLocations(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
m_globalUboBackendBlockIndex = -1;
|
||||
m_globalUboBackendBlockSize = 0;
|
||||
m_lastUploadedGlobalUboVersion = ~0u;
|
||||
m_globalUboRingAllocation = {};
|
||||
if (stateProgramObject->GetUBOSize() > 0) {
|
||||
const Uint blockIndex =
|
||||
g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
||||
if (blockIndex != GL_INVALID_INDEX) {
|
||||
m_globalUboBackendBlockIndex = static_cast<Int>(blockIndex);
|
||||
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 {
|
||||
MGLOG_W("Program %u has frontend global UBO storage, but backend has no %s block.",
|
||||
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).
|
||||
void TrimBufferPool();
|
||||
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 VertexArrayImpl {
|
||||
@@ -392,6 +429,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<SamplerUniformBinding>& GetSamplerUniformBindings() { return m_samplerUniformBindings; }
|
||||
Uint32 GetLastUploadedGlobalUboVersion() const { return m_lastUploadedGlobalUboVersion; }
|
||||
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
|
||||
// built from; a mismatch means every link-derived cache here is stale.
|
||||
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
||||
@@ -410,9 +452,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool m_isInitialized = false;
|
||||
|
||||
Int m_globalUboBackendBlockIndex = -1;
|
||||
Int m_globalUboBackendBlockSize = 0;
|
||||
Vector<Int> m_uniformBlockBackendIndices; // frontend block index -> backend index (-1 = absent)
|
||||
Vector<SamplerUniformBinding> m_samplerUniformBindings;
|
||||
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||
Uint32 m_syncedLinkVersion = ~0u;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user