Files
MobileGL/MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp
T

2990 lines
183 KiB
C++

// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "ManagersInternal.h"
namespace MobileGL::MG_Backend::DirectGLES {
constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false;
namespace BufferImpl {
namespace {
// GL_ARRAY_BUFFER redundant-bind cache (id 0 = unknown/none).
Uint g_boundArrayBufferId = 0;
Bool g_boundArrayBufferKnown = false;
// Driver-level GL_PIXEL_PACK/UNPACK_BUFFER binding shadows (see
// Managers.h). Resting state between operations is 0; scopes in the
// readback/upload paths bind what they need through the cache and
// return to 0, so a stale user PBO can never capture a later
// readback that meant to target client memory.
Uint g_boundPixelPackBufferId = 0;
Bool g_boundPixelPackBufferKnown = false;
Uint g_boundPixelUnpackBufferId = 0;
Bool g_boundPixelUnpackBufferKnown = false;
} // namespace
// Bumped whenever the backend ES context is destroyed; resources with
// an older generation hold ids from a dead context.
Uint g_bufferContextGeneration = 1;
namespace {
// Buffer-mutation epoch backing store (contract, mutation-site list and
// memory-ordering rules: Managers.h at the accessor declarations).
// Starts at 1 so the memo stamps' 0 means "never stamped". Atomic:
// frontend buffer ops may run on non-draw threads while the draw thread
// reads; the release-bump-AFTER-mutation / acquire-read-BEFORE-probes
// pairing makes a stamp taken against stale state impossible to consume.
std::atomic<Uint64> g_bufferMutationEpoch{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 bindings - indexed and pixel
// pack/unpack alike - to 0, and a recycled name matching a stale shadow
// entry would otherwise false-skip the rebind).
void ScrubBufferBindingShadowsForId(Uint id);
// Defined next to the same shadow, and the counterpart to the scrub above for a
// buffer whose STORE was re-specified rather than deleted: the binding survives -
// nothing unbound the id - but the extent the driver resolved for it at bind time
// does not. Marks those points unknown so the next sync issues a real
// glBindBufferBase/Range instead of skipping it.
void InvalidateIndexedBufferBindingShadowsForId(Uint id);
// Resources whose owning BufferObject died; ids deleted at the next
// sync point with a current ES context.
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
std::mutex g_deferredBufferReleasesMutex;
// Cheap emptiness probe so the per-draw drain can skip the mutex and
// context check when nothing was enqueued (the overwhelmingly common
// case). Written only under the mutex; read lock-free.
std::atomic<Bool> g_hasDeferredBufferReleases{false};
// --- Buffer-storage pool (buffer-object recycling) -----------------------
// 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.immutableStorage && 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();
}
// Pooling keeps the id alive (and thus any driver binding of it);
// drop to unknown rather than claiming the post-delete 0 state.
if ((g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == r.id) ||
(g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == r.id)) {
InvalidatePixelBufferBindingCaches();
}
const std::lock_guard<std::mutex> lock(g_poolMutex);
auto& bucket = g_bufferPool[r.storageSize];
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
ScrubBufferBindingShadowsForId(r.id);
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;
}
// A dead context's ring buffers: shared objects, so they outlive it; deleted at the next
// Present of any context of the same share-group generation.
struct OrphanRingBuffer {
Uint id = 0;
Uint contextGeneration = 0;
};
std::mutex g_orphanRingBuffersMutex;
Vector<OrphanRingBuffer>& OrphanRingBuffers() {
static auto* orphans = new Vector<OrphanRingBuffer>();
return *orphans;
}
// Each set's generations start in a range of their own: a per-program slot cache keyed
// by (generation, offset) must never take one context's ring for another's.
std::atomic<Uint32> g_ringGenerationBases{0};
} // namespace
RingSet::RingSet() {
const Uint32 base = (g_ringGenerationBases.fetch_add(1, std::memory_order_relaxed) + 1) << 12;
ubo.store.generation = base;
unpack.store.generation = base;
upload.store.generation = base;
}
RingSet::~RingSet() {
const std::lock_guard<std::mutex> lock(g_orphanRingBuffersMutex);
for (const PersistentRing* ring : {&ubo, &unpack, &upload}) {
if (ring->store.id != 0) {
OrphanRingBuffers().push_back({ring->store.id, ring->store.contextGeneration});
}
for (const auto& retired : ring->retired) {
if (retired.id != 0) OrphanRingBuffers().push_back({retired.id, retired.contextGeneration});
}
}
}
DirectGLES::PerNativeContext<RingSet> g_ringSets;
namespace {
PersistentRing& UboRing() {
return g_ringSets.Current().ubo;
}
PersistentRing& UnpackRing() {
return g_ringSets.Current().unpack;
}
PersistentRing& UploadRing() {
return g_ringSets.Current().upload;
}
// 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. The
// generation counter must survive the reset — frame serials also survive
// context recreation, so a restarted counter could revalidate a stale
// per-program slot cache against the new ring.
void ResetRingForNewContext(PersistentRing& ring) {
const Uint32 keptGeneration = ring.store.generation;
ring.store = {};
ring.store.generation = keptGeneration;
ring.store.contextGeneration = g_bufferContextGeneration;
// Keep the ring's own alignment across the wipe: the slow path rounds a
// request with it BEFORE the store that would set it exists.
if (ring.fixedAlignment != 0) ring.store.alignment = ring.fixedAlignment;
ring.retired.clear();
ring.frameMarks.clear();
}
// P3a: ONE body per helper, parameterised on where its host bytes and its
// extent come from, so the legacy arm and the handle arm of a push build share
// the three-tier flush, the respecify and the range upload rather than owning a
// copy each - a second copy of the tier ladder is exactly how a tier changes in
// silence. The #else below is the PULL build, whose text must stay byte-identical
// to the pre-P3a one (G1: the pull build gains no symbol and resizes none); it is
// frozen by that gate and retires with the pull path at P13.
// (Re)specify backend storage from the shadow copy: glBufferData.
// The orphaning point - the ES driver performs the actual rename.
// TODO(buffer-pool Phase 2): orphan-on-respecify is NOT yet implemented.
// When the current id is BUSY (lastUseFrameSerial > CompletedFrameSerial())
// && !persistentMapped && !noOrphan, express the orphan as an id-swap
// (retire the busy id into the pool, bind a fresh/pooled id) instead of the
// in-place glBufferData below, to avoid the driver's own rename/stall. Not
// pursued yet: glBufferData/glBufferSubData currently sit below profiler
// noise, so respecify is not a hot path in the profiled scenes.
// The body both arms run. `size`, `usage`, `initialData` and `syncedSerial` are the
// four things the legacy arm reads off the frontend object and the handle arm reads
// off the applier's stored descriptor and the shadow base the call carried; nothing
// else in here differs, so there is ONE glBufferData and one extent rule rather
// than two that can drift apart.
void RespecifyStorageWith(GLESBufferResource& resource, SizeT size, GLenum usage, const void* initialData,
Uint64 syncedSerial) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Read BEFORE the fields below are overwritten: whether this respecify changes
// the store's EXTENT is what decides if the indexed-binding shadow still
// describes the driver.
const Bool extentChanged = !resource.storageInitialized || resource.storageSize != size;
BindBufferId(TempBufferTarget, resource.id);
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage);
if (MG_Util::PipeStats::Enabled() && initialData != nullptr) {
// An ORPHANING respecify passes NULL and moves nothing, which is exactly
// why the test is on initialData rather than on size.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
resource.storageSize = size;
resource.storageInitialized = true;
resource.pendingRespecify = false;
resource.pendingRanges.clear();
resource.pendingResidentWrites.clear();
resource.syncedChangeSerial = syncedSerial;
// A GROWN store keeps its indexed bindings, and BindBufferBaseCached skips a
// rebind whenever the shadow already records this id at that index - so on a
// driver that resolves a whole-buffer indexed binding's extent at BIND time
// (Adreno does; Mali does not) the shader keeps seeing the old, smaller range:
// stores past it are dropped and loads return zero. Forget what the shadow
// claims for this id so the next SyncBufferBindingPoints issues the bind for
// real. Only when the extent actually moved: an orphaning respecify at the same
// size is Minecraft's per-frame hot path and its bindings are still exact.
if (extentChanged) {
InvalidateIndexedBufferBindingShadowsForId(resource.id);
}
}
void RespecifyStorageNow(GLESBufferResource& resource, BufferObject& bufferObject) {
const SizeT size = bufferObject.GetSize();
// An orphaning respecify (glBufferData with NULL, content never
// written since) stays a pure NULL reallocation: the driver renames
// the store without a stall and nothing is transferred. Uploading
// the stale shadow here turned Minecraft-style orphaning into a
// full-size synchronized upload.
const void* initialData =
(size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr;
RespecifyStorageWith(resource, size, MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage()),
initialData, bufferObject.GetChangeSerial());
}
Bool StorageMatchesSize(const GLESBufferResource& resource, SizeT size) {
return resource.storageInitialized && !resource.pendingRespecify && resource.storageSize == size;
}
Bool StorageMatches(const GLESBufferResource& resource, const BufferObject& bufferObject) {
return StorageMatchesSize(resource, bufferObject.GetSize());
}
} // namespace
// -------------------------------------------------------------------------------
// R-11 - THE SERVER'S OWN COPY OF THE STAGED BYTES. Package v1.
//
// The rule, the evidence and the reason the storage is a side table rather than a
// member of GLESBufferResource are all in MG_Remote/Server/StagedShadow.h. This is
// only the instance and the four call sites.
//
// ONE PER PROCESS, and its copying arm is decided ONCE at first use: the two arms
// hold the authoritative bytes in DIFFERENT places, so an answer that changed
// mid-run would strand every resource already staged - the same reason
// ResolveResourceSubsystemArm latches (Managers.h). Leaked at exit like every other
// MG_Remote singleton (ID-8): ~BufferObject reaches the destroy path from exit
// handlers, after this TU's globals would already be gone.
// -------------------------------------------------------------------------------
MG_Record::StagedShadowStore& ServerStaged() {
static MG_Record::StagedShadowStore& store =
*new MG_Record::StagedShadowStore(MG_Config::Transport != MG_Config::TransportMode::Monolith);
return store;
}
namespace {
#if MOBILEGL_BUILD_DISAGGREGATED
// -------------------------------------------------------------------------------
// P11 B2 (T0) - WHEN AN IMPORTED STORE'S AHardwareBuffer MAY BE LET GO.
//
// A T0 store is the client's AHardwareBuffer imported with glBufferStorageExternalEXT
// (Ops_H_ImportExternal below). The twin holds its own reference, and every path that
// ends the store - a respecify, a destroy, a re-import - deletes the GL id and then
// RETIRES the reference here: a fence is inserted after the delete, and the reference is
// released only once that fence has signalled, i.e. once every command that could still
// read or write the pages has completed on this context's GPU. The client's own
// reference ends independently (it releases on respecify/destroy); the pages live until
// the last of the two. No link watermark is involved (F-L2): the fence is this server's.
//
// Swept (non-blocking) at every import, every deferred-release drain and every ensure;
// a context loss releases the lot, because the GPU work those fences named ended with
// the context. Apply thread only, like every GL call here.
// -------------------------------------------------------------------------------
struct T0RetiredImport {
GLsync fence = nullptr;
void* ahb = nullptr;
Uint generation = 0;
};
Vector<T0RetiredImport> g_t0Retired;
// T0 twins whose destroy found no current context: Ops_OnDestroy queued them on the
// deferred-release list and keeps a second reference here, so the reference is
// retired only after that drain deleted the id (ProcessDeferredT0Retires). Guarded by
// g_deferredBufferReleasesMutex, like the list it shadows.
Vector<SharedPtr<BackendBufferResource>> g_t0DeferredDestroys;
std::atomic<Bool> g_hasT0DeferredDestroys{false};
Uint64 g_t0ImportsLive = 0;
void SweepT0Retired(Bool contextGone) {
for (SizeT i = 0; i < g_t0Retired.size();) {
T0RetiredImport& retired = g_t0Retired[i];
const Bool sameContext =
!contextGone && retired.generation == g_bufferContextGeneration && CanTouchGLNow();
Bool done =
contextGone || retired.generation != g_bufferContextGeneration || retired.fence == nullptr;
if (!done && sameContext && g_GLESFuncs.glClientWaitSync) {
const GLenum status = g_GLESFuncs.glClientWaitSync(retired.fence, 0, 0);
done = status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED ||
status == GL_WAIT_FAILED;
}
if (!done) {
++i;
continue;
}
if (retired.fence != nullptr && sameContext && g_GLESFuncs.glDeleteSync)
g_GLESFuncs.glDeleteSync(retired.fence);
MG_Remote::Transport::AdoptT0::ReleaseImported(retired.ahb);
retired = g_t0Retired.back();
g_t0Retired.pop_back();
}
}
// Call AFTER the store's GL id is deleted (or known dead): the fence then follows every
// command that used it. Clears the twin's reference whatever the context state.
void RetireT0Import(GLESBufferResource& resource) {
if (resource.externalAhb == nullptr) return;
T0RetiredImport retired;
retired.ahb = resource.externalAhb;
retired.generation = resource.contextGeneration;
if (CanTouchGLNow() && resource.contextGeneration == g_bufferContextGeneration &&
g_GLESFuncs.glFenceSync) {
retired.fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
resource.externalAhb = nullptr;
if (g_t0ImportsLive > 0) --g_t0ImportsLive;
g_t0Retired.push_back(retired);
SweepT0Retired(false);
}
#else
// No transport, no imported store: nothing ever needs retiring (P13 W5).
inline void RetireT0Import(GLESBufferResource&) {}
#endif // MOBILEGL_BUILD_DISAGGREGATED
#define MGL_SERVER_STAGED_DROP(res) ServerStaged().Drop(&(res))
#define MGL_SERVER_STAGED_DROP_ALL() ServerStaged().DropAll()
#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) \
ServerStaged().RequireCoverage(&(res), (base), (start), (end), (site))
// The host bytes a range upload/flush reads. On the legacy arm the frontend object's
// shadow; on the handle arm the base the last content-carrying call handed over.
void UploadRangeFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, SizeT end) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (start >= end || hostBase == nullptr) return;
MGL_SERVER_STAGED_REQUIRE(resource, hostBase, start, end, "upload_range");
BindBufferId(TempBufferTarget, resource.id);
g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start),
hostBase + start);
}
void UploadRangeNow(GLESBufferResource& resource, BufferObject& bufferObject, SizeT start, SizeT end) {
UploadRangeFrom(resource, bufferObject.MappedData(), start, end);
}
Bool RingAvailable(PersistentRing& ring);
// True when a pending-range flush can go through the staging ring right
// now: kill switch off, the ES copy entry point resolved, and the ring's
// own availability gate (EXT_buffer_storage + fences + live context) up.
Bool UploadRingUsableNow() {
if (MG_Config::Features.EsprytDisableUploadRing) return false;
if (!g_GLESFuncs.glCopyBufferSubData) return false;
return RingAvailable(UploadRing());
}
// A partial range below this goes through the staging ring instead of a
// range-invalidating map: the map's page-substitution fast path needs a
// sizeable (page-coverable) range to engage, and below it the driver
// falls back to waiting out the WAR hazard on the CPU.
constexpr SizeT kInvalidateRangeMinBytes = 128u * 1024u;
// The push arm's copy of this threshold lives in Managers.h, where a unit case can
// reach it (InvalidateFlushAccessFor). Two constants, one value, and the compiler
// is what keeps them one: the pull arm's FlushPendingRangesNow is byte-frozen
// against 5cb826b0 (ID-15), so the constant it reads may not move to a header.
static_assert(kInvalidateRangeMinBytes == kEsprytInvalidateRangeMinBytes,
"the two arms of the three-tier ladder must use the same tier-1 threshold");
} // namespace
void FlushPendingRangesFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize,
SizeT hostBaseFrom, SizeT hostBaseTo) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
VecRange1D ranges;
{
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
if (resource.pendingRanges.empty()) return;
ranges = std::move(resource.pendingRanges);
resource.pendingRanges.clear();
}
// No shadow to read from: only reachable on the handle arm, where the base
// arrives with the call, and only for a resource that queued a range before any
// content-carrying call named one. The queue is already drained, so the next
// full re-upload is what puts the store right.
if (hostBase == nullptr) return;
// Clamp against BOTH extents: the readback flush may run while the
// frontend size and the backend store disagree (a pending respecify
// resolves that later; bytes past either end have nowhere to land).
const SizeT limit = std::min(frontendSize, resource.storageSize);
const Bool mapUsable = !MG_Config::Features.EsprytDisableInvalidateFlush && g_GLESFuncs.glMapBufferRange &&
g_GLESFuncs.glUnmapBuffer;
const Bool ringUsable = UploadRingUsableNow();
for (const auto& range : ranges) {
const SizeT end = std::min(range.end, limit);
const SizeT start = std::min(range.start, end);
const SizeT size = end - start;
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
// Counted once per queued range, before the three delivery shapes
// below diverge: all three move exactly these bytes, and it is the
// byte count - not the shape - that sizes SEG_STAGE.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
// The invalidating map's fast path is SHAPE-dependent on this Mali
// driver: a whole-buffer invalidation renames the store outright,
// and a large range gets fresh pages - but a small unaligned range
// of a busy store makes the map WAIT (osup_sync_object_wait, ~9%
// of a Minecraft 26.3 replay). So: whole buffer -> orphan-map;
// large range -> range-invalidating map; small range -> the staged
// ring copy, whose worst case (a whole-destination ghost) is only
// ever the small destination itself.
//
// The map covers EXACTLY the queued range: only those bytes are the
// shadow's to rewrite. Widening to page bounds looked free and was
// not - the widened bytes clobbered GPU-written data (an SSBO
// counter beside the app's SubData) with the stale shadow.
// The extent the bytes behind `hostBase` are actually good for, clamped
// to the store. With the default it IS [start, end), so the refusal below
// cannot fire and the ladder is unchanged; with a real SEG_STAGE snapshot
// it is the snapshot's window and a disagreement drops this range to the
// staging ring instead of letting a widened INVALIDATE_RANGE declare bytes
// dead that nothing is about to rewrite.
const SizeT coveredFrom = hostBaseFrom > start ? hostBaseFrom : start;
const SizeT coveredTo = hostBaseTo < end ? hostBaseTo : end;
#if MOBILEGL_PIPE_VERIFY
if (coveredFrom != start || coveredTo != end) {
MGLOG_E_ONCE("MGPipe: Fatal{StageSnapshotTooNarrow} flush_pending_ranges: the "
"staged bytes cover [%zu, %zu) and the queued range is [%zu, %zu) "
"- tiers 2 and 3 would copy from outside the snapshot",
hostBaseFrom, hostBaseTo, start, end);
}
#endif
// Read per range, like the pull arm: a ring copy staged for an earlier range of
// this same drain puts every later partial range of the store behind it
// (InvalidateFlushAccessFor's queued-copy rule, dev f973008c).
const Bool ringCopyInFlight = resource.ringCopyRetireSerial > DirectGLES::CompletedFrameSerial();
const GLbitfield access = mapUsable
? InvalidateFlushAccessFor(start, end, coveredFrom, coveredTo, limit,
resource.storageSize, ringCopyInFlight)
: 0u;
if (access != 0) {
BindBufferId(TempBufferTarget, resource.id);
void* dst =
g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start, (GLsizeiptr)size, access);
if (dst) {
Memcpy(dst, hostBase + start, size);
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
continue;
}
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes && RingAllocate(UploadRing(), size, ringOffset)) {
Memcpy(UploadRing().store.mappedPtr + ringOffset, hostBase + start, size);
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, (GLintptr)ringOffset,
(GLintptr)start, (GLsizeiptr)size);
// Commands recorded now retire with the fence the next Present inserts.
resource.ringCopyRetireSerial = DirectGLES::CurrentFrameSerial() + 1;
} else {
UploadRangeFrom(resource, hostBase, start, end);
}
}
}
namespace {
// THE TWO-ARM SHAPE, AND BOTH ARMS ARE HASHED FROM HERE ON (ID-15, which
// supersedes ID-13's "FlushPendingRangesNow is defined exactly once, outside any
// `#if`").
//
// This file has TWO three-tier drains, and exactly one of them is compiled into any
// given build: `FlushPendingRangesFrom` above, inside `#if MOBILEGL_PIPE_PUSH`, is
// what a PUSH build runs - both of its call sites, the legacy one and
// Ops_H_Readback, reach it - and `FlushPendingRangesNow` inside the `#else` below,
// byte-identical to 5cb826b0, is what a PULL build runs. A push build compiles no
// FlushPendingRangesNow at all.
//
// NO FORWARDER HERE, which is what forced the two-arm shape: G5's extractor is
// preprocessor-blind, so a `FlushPendingRangesNow` that forwarded onto the ladder
// above would leave TWO definitions of that one name in the file and the gate would
// exit 2 - a gate that cannot run - rather than compare anything. (The handle arm
// cannot call FlushPendingRangesNow itself either: its signature takes a
// BufferObject&, and having no frontend object to offer is the whole point of the
// conversion.)
//
// WHAT ID-15 ADDS. The shape above is sound, but a gate keyed on the NAME
// `FlushPendingRangesNow` alone would from here on protect only the text the
// shipping build never compiles: a tier-threshold or map-access-bit edit made in
// FlushPendingRangesFrom - the ladder that actually runs, and the one MC 26.3's p99
// depends on - would satisfy both G1 (the pull text did not move) and G5 (the pull
// name still hashes the same). So scripts/p3a_untouched_regions.sh hashes BOTH
// names: eleven functions, the pull ladder compared against the P3a base ref and
// this one against a sha pinned at 3e298c9a. Neither ladder may drift, and neither
// may drift AWAY FROM THE OTHER without the gate saying so.
// P15: the small-store write rename (WholeStoreRenameApplies, Managers.h), applied to the
// queue BEFORE the drain takes it. The ladder above is pinned and is not touched: it is
// handed one whole-store range, which its own tier 1 already answers with the
// whole-buffer orphan-map. Draw path only - the readback drains keep their exact ranges.
void RenameSmallStoreForDrain(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize) {
const SizeT store = resource.storageSize;
const Bool mapUsable = !MG_Config::Features.EsprytDisableInvalidateFlush &&
g_GLESFuncs.glMapBufferRange && g_GLESFuncs.glUnmapBuffer;
const Bool foreignBytes = resource.serverGpuWritten || resource.persistentMapped ||
resource.immutableStorage || resource.externalAhb != nullptr;
// The size gate first, with the whole store as the queue: it answers every refusal
// that does not depend on the queue before any lock is taken.
if (!WholeStoreRenameApplies(store, frontendSize, store, hostBase != nullptr, foreignBytes, mapUsable))
return;
// Under split the host bytes are the server's staged copy, which only holds what the
// content records delivered; monolith's base is the frontend shadow, the whole store.
if (ServerStaged().CopiesIntoServerStorage() && !ServerStaged().IsCovered(&resource, 0, store)) return;
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
if (resource.pendingRanges.empty() || !resource.pendingResidentWrites.empty()) return;
SizeT queuedBytes = 0;
for (const auto& range : resource.pendingRanges) {
const SizeT end = std::min(range.end, store);
queuedBytes += end - std::min(range.start, end);
}
if (!WholeStoreRenameApplies(store, frontendSize, queuedBytes, true, false, true)) return;
resource.pendingRanges.clear();
resource.pendingRanges.Add({0, store});
}
} // namespace
// Land the app bytes queued for an ADOPTED store on the GPU timeline: staged
// into the upload ring and delivered by glCopyBufferSubData. The destination
// is the IMMUTABLE persistent store, which the driver can neither rename nor
// ghost, so the copy is plain job ordering - after every in-flight reader,
// before the next consumer - which is exactly glBufferSubData's contract.
// (The in-place host write these bytes replaced tore the frames still
// reading the old vertex data: one-frame wrong geometry during fast camera
// movement.) Fallback: direct glBufferSubData - the adopted store carries
// DYNAMIC_STORAGE, and immutability again forbids the whole-store ghost.
//
// It never read the frontend object (the bytes are on the resource's own queue and
// the extent is the backend store's), so it takes none: the handle arm calls exactly
// this function with exactly these semantics.
void DrainResidentWritesNow(GLESBufferResource& resource) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
Vector<GLESBufferResource::PendingResidentWrite> writes;
{
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
if (resource.pendingResidentWrites.empty()) return;
writes = std::move(resource.pendingResidentWrites);
resource.pendingResidentWrites.clear();
}
const SizeT limit = resource.storageSize;
const Bool ringUsable = UploadRingUsableNow();
for (const auto& write : writes) {
if (write.offset >= limit) continue;
const SizeT size = std::min(write.bytes.size(), limit - write.offset);
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes && RingAllocate(UploadRing(), size, ringOffset)) {
Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size);
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, (GLintptr)ringOffset,
(GLintptr)write.offset, (GLsizeiptr)size);
} else {
BindBufferId(TempBufferTarget, resource.id);
g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)write.offset, (GLsizeiptr)size,
write.bytes.data());
}
}
}
namespace {
// EXT_buffer_storage bit values (same numeric values as the desktop ARB
// tokens); defined locally so this compiles regardless of which GLES headers
// expose the EXT tokens.
constexpr GLbitfield kMapPersistentBit = 0x0040;
constexpr GLbitfield kMapCoherentBit = 0x0080;
constexpr GLbitfield kDynamicStorageBit = 0x0100;
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
if (!resource) return;
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
if (glesResource->contextGeneration != g_bufferContextGeneration) {
glesResource->id = 0; // id belonged to a destroyed context
RetireT0Import(*glesResource); // P11 B2: its GL store died with that context
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();
}
ScrubBufferBindingShadowsForId(glesResource->id);
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
glesResource->id = 0;
}
// P11 B2: a T0 store's AHardwareBuffer goes only after a fence behind the delete.
RetireT0Import(*glesResource);
return;
}
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
#if MOBILEGL_BUILD_DISAGGREGATED
if (glesResource->externalAhb != nullptr) { // P11 B2: retired after the drain deletes it
g_t0DeferredDestroys.push_back(resource);
g_hasT0DeferredDestroys.store(true, std::memory_order_release);
}
#endif
g_deferredBufferReleases.push_back(std::move(resource));
g_hasDeferredBufferReleases.store(true, std::memory_order_release);
}
// ResourceContentIsDeclared MOVED OUT OF THIS ANONYMOUS NAMESPACE (P3b/P4b espryt
// D1 slice 1) to sit beside RequireStagedCoverage at namespace scope: the XFB
// scatter in DirectGLES.cpp has to ask the same M-3 question this file's pool-reuse
// ladder asks, and the two must not be able to answer it differently.
void Ops_H_Create(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc) {
(void)res;
(void)desc;
// Nothing, and that is the row D-A2 writes: storage is defined lazily by the
// first resource_respecify, and the ensure path already tolerates a resource
// with none. Minting the twin here would only move the allocation earlier.
}
void Ops_H_Respecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
const void* initialBytes) {
auto* resource = FindBufferResourceForHandle(res);
// P13 W4a (§1.4): ON MONOLITH'S RECORD ARM THE TWIN IS MINTED HERE, not lazily at
// the first draw. The record arm's readers have no frontend object to re-read a
// base from, so the base this call carries - the client's shadow, for an orphaning
// respecify too (PipeFill passes it there on this arm) - is the only way they learn
// it. Minting is GL-free; the storage stays lazy.
const Bool aliasesFrontend = MG_Config::RecordArmAliasesFrontend();
if (resource == nullptr && aliasesFrontend && initialBytes != nullptr) {
resource = GetOrCreateBufferResourceForHandle(res);
}
if (resource != nullptr) {
// A respecify's companion pointer IS the shadow base (offset 0) - and an
// ORPHANING one (glBufferData with NULL) carries HasDefinedContent clear and
// therefore no pointer at all. That is also the call that RESIZES the
// client's shadow (PipeResource::ResizeShadow is reserve + resize, so a grow
// past the reserve reallocates and frees the block a previous call's base
// pointed into), so the recorded base is dead exactly when no new one
// arrives. FORGET it here rather than leave a pointer nothing refreshes:
// every reader below treats a null base as "no bytes to move", which is the
// honest answer, and the ensure path re-reads the live base from the
// frontend object it still holds.
// P13 W4a: an ALIASING store records the base whether or not the content is
// defined - the pointer is the frontend shadow's new allocation either way, and
// the upload below stays gated on the descriptor (RespecifyStorageWith's
// initialData), so an orphan still uploads nothing.
if ((desc.HasDefinedContent != 0 || aliasesFrontend) && initialBytes != nullptr) {
// R-11: in monolith this is the client's shadow base, unchanged; under
// split it is copied into server-owned storage first. A respecify's
// companion pointer is the base at offset 0 and covers the whole store.
// Under split it is ALWAYS null (contract table 1 row 19 -
// initialBytes does not cross, and the content arrives as
// resource_subdata records right behind this record), so in practice
// this arm is the monolith one and the else arm is the split one.
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, static_cast<SizeT>(desc.Width),
initialBytes, 0, static_cast<SizeT>(desc.Width));
} else {
MGL_SERVER_STAGED_DROP(*resource);
resource->hostBytes = nullptr;
}
// P8-C: whatever a shader wrote belonged to the store this call replaces.
resource->serverGpuWritten = false;
resource->contentSinceRespecify = false;
}
if (!resource) return; // lazy: the ensure path full-uploads on creation
if (resource->immutableStorage) {
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
if (resource->id != 0 && CanTouchGLNow() &&
resource->contextGeneration == g_bufferContextGeneration) {
NoteBufferIdDeleted(resource->id);
// Frontend VAO bindings survive respecification; force their
// backend twins to bind the replacement buffer name. (dev@d7655247,
// carried into this arm as well: the handle arm duplicates the
// immediate retire path, so a fix that lands in only one of the two
// leaves the bug alive on whichever arm the operator selects.)
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
}
// P11 B2: a respecified T0 store ends here on the server's side too. Its
// AHardwareBuffer is released only after a fence behind the delete (or at once
// when the store's context is already gone) - never under a frame in flight,
// however the client's own release (at this respecify) raced it.
RetireT0Import(*resource);
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
if (!CanTouchGLNow() || resource->id == 0 || resource->contextGeneration != g_bufferContextGeneration) {
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
if (desc.Width == 0) {
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = false;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
// Same orphaning rule, expressed on the descriptor: a NULL-data respecify has
// HasDefinedContent clear and the client sends no bytes for it.
const void* initialData = desc.HasDefinedContent != 0 ? initialBytes : nullptr;
RespecifyStorageWith(*resource, static_cast<SizeT>(desc.Width),
MG_Util::ConvertBufferUsageToGLEnum(static_cast<BufferUsage>(desc.Usage)),
initialData, ResourceSerialOf(res));
}
void Ops_H_SubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, const void* bytes) {
const SizeT offset = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferOffset(record));
const SizeT size = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferSize(record));
auto* resource = FindBufferResourceForHandle(res);
// R-11 / ID-52 item 3: under an ACTIVE TRANSPORT this record's bytes are the ONLY
// delivery of the buffer's content - the client sends no companion pointer and the
// server has no MappedData to fall back on. But the twin is created LAZILY at the
// first draw (Ops_H_Create is a no-op by D-A2), which is AFTER this record, so a
// subdata that finds no twin would return here and the bytes would be lost - the
// draw then uploads an empty/shifted store (TriangleScenario read a blue triangle
// before this). So under split the subdata MINTS the twin it needs to stage into.
// Monolith is untouched: the twin stays lazy there because the frontend object's
// MappedData is the source and nothing is lost by deferring the allocation (D-A2).
if (resource == nullptr && bytes != nullptr) {
resource = GetOrCreateBufferResourceForHandle(res);
}
if (!resource) return;
// P11 B2: A T0 STORE TAKES A SERVER-ORIGINATED WRITE GPU-ORDERED. Its pages are the
// client's AHardwareBuffer, so there is no staged copy to refresh and the adopted
// early return below ("the coherent map already has the bytes") is false for bytes
// the SERVER produced - P9 W1's pack-buffer landing (PipeApplier.cpp
// LandReadbackInBuffer) is the one producer, and it would lose its pixels there. They
// are queued exactly as a resident SubData is (Ops_H_ResidentSubData): landed after
// every command already recorded, before the next one that reads the store.
if (resource->externalAhb != nullptr && bytes != nullptr && size != 0) {
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
auto& write = resource->pendingResidentWrites.emplace_back();
write.offset = offset;
const auto* source = static_cast<const Uint8*>(bytes);
write.bytes.assign(source, source + size);
}
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
// P13 W4b: A SERVER-ORIGINATED WRITE INTO AN ALIASING STORE (monolith's record arm)
// lands in the GL store, never in the base: the bytes are the sink's scratch, and the
// base is the frontend's shadow, which this side does not write. Queued uploads land
// first, so they cannot later overwrite these bytes from the stale shadow; the store
// is then marked GPU-written, which sends the frontend (its own pack-buffer mark) and
// every CPU reader here (SplitHostBytesForCpuRead) back to the GL store for them.
if (MG_Pipe::MGPipeApplyingServerOriginatedWrite() && MG_Config::RecordArmAliasesFrontend() &&
bytes != nullptr && size != 0) {
// The store must exist to take them: a pack buffer is often never drawn from, so
// nothing else has built it. Only a store that does not exist yet is ensured: the
// applier has already counted THIS write into the record's serial, so the ensure
// of a built store would read it as an untracked mutation and re-upload the
// shadow over the runs this landing wrote before it.
if (CanTouchGLNow() &&
(resource->id == 0 || !resource->storageInitialized || resource->pendingRespecify ||
resource->contextGeneration != g_bufferContextGeneration)) {
resource = EnsureBufferResourceForHandle(nullptr, res);
}
if (resource != nullptr && CanTouchGLNow() && resource->id != 0 && resource->storageInitialized &&
!resource->persistentMapped && !resource->pendingRespecify &&
resource->contextGeneration == g_bufferContextGeneration &&
StorageMatchesSize(*resource, ResourceWidthOf(res))) {
FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res));
BindBufferId(TempBufferTarget, resource->id);
g_GLESFuncs.glBufferSubData(TempBufferTarget, static_cast<GLintptr>(offset),
static_cast<GLsizeiptr>(size), bytes);
// The store holds everything the record now says (the applier bumped its
// serial for this write): without the stamp the next ensure would see a
// newer serial and re-upload the shadow over the bytes that just landed.
resource->syncedChangeSerial = ResourceSerialOf(res);
} else if (resource != nullptr) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
auto& write = resource->pendingResidentWrites.emplace_back();
write.offset = offset;
const auto* source = static_cast<const Uint8*>(bytes);
write.bytes.assign(source, source + size);
}
if (resource != nullptr) resource->serverGpuWritten = true;
return;
}
// M-2: UNDER pendingMutex, because this line runs BEFORE the CanTouchGLNow()
// test below - i.e. on the arm D-A2 deliberately keeps reachable off the render
// thread - while every reader of hostBytes (Ops_H_Readback's drain, the
// kill-switch map arm of Ops_H_FlushRange, the ensure path, the fp64 narrowing)
// is on the render thread. It was the one member of this struct that the
// off-thread path touched with neither a lock nor an atomic: pendingRanges and
// pendingResidentWrites are under this mutex and syncedChangeSerial is an
// std::atomic read with acquire. The lock it is put under is deliberately THAT
// one and not a new one - the base and the queued range are one fact ("these
// bytes, at this base"), the drain takes this mutex to lift the ranges, and a
// drain that sees a range therefore sees the base that range was queued
// against. The store is kept on both arms rather than restricted to the
// CanTouchGLNow() one because Ops_H_Readback can drain with no draw in between,
// and the ensure path's republication (the 3c55e027 fix) is what runs at a draw.
if (bytes != nullptr) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
// R-11: MONOLITH keeps the client's base; SPLIT copies [offset, offset+size)
// into server-owned storage and points hostBytes at that. Inside the SAME
// lock as the queued range, because the base and the range are one fact
// ("these bytes, at this base") and the drain takes this mutex to lift them.
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, offset, size);
if (size != 0) resource->contentSinceRespecify = true;
}
if (resource->pendingRespecify) return; // full re-upload pending anyway
if (!CanTouchGLNow() || resource->id == 0 || resource->contextGeneration != g_bufferContextGeneration ||
!StorageMatchesSize(*resource, ResourceWidthOf(res))) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({offset, offset + size});
return;
}
// The adopted zero-copy store already HAS the bytes; a driver upload here would
// re-synchronize what coherent mapping made free.
if (resource->persistentMapped && resource->persistentPtr) {
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
if (MG_Config::Features.EsprytDisableUploadRing) {
UploadRangeFrom(*resource, resource->hostBytes, offset, offset + size);
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
// The Mali WAR-stall fix, unchanged: queue and let draw-time sync stage the
// merged ranges through the upload ring.
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({offset, offset + size});
}
void Ops_H_ResidentSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record,
const void* bytes) {
const SizeT offset = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferOffset(record));
const SizeT size = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferSize(record));
auto* resource = FindBufferResourceForHandle(res);
if (!resource || size == 0 || bytes == nullptr) return;
// DELIBERATELY not stored as hostBytes: these are the application's staging
// store and are valid for the duration of the call only (BufferObject.h:84-85),
// which is exactly why they are COPIED here rather than referenced later.
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
auto& write = resource->pendingResidentWrites.emplace_back();
write.offset = offset;
const auto* source = static_cast<const Uint8*>(bytes);
write.bytes.assign(source, source + size);
}
void Ops_H_FlushRange(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPFlushRange& record, const void* bytes) {
auto* resource = FindBufferResourceForHandle(res);
if (!resource) return;
const SizeT start = static_cast<SizeT>(record.Offset);
const SizeT end = start + static_cast<SizeT>(record.Size);
// M-2, the second store site - same lock, same reason as Ops_H_SubData's, and
// the same R-11 copy. UNDER SPLIT `bytes` IS ALWAYS NULL HERE by ruling (C-6 /
// contract table 1 row 20: resource_flush_range carries no bytes at all - the
// ladder it drives rewrites its range from the authoritative shadow, which rule
// C makes server-owned, so resource_subdata is already the only way bytes reach
// it and a second carrier would be a forgeable way to say the same thing). So
// this arm keeps whatever the preceding subdata records staged, which is
// exactly what the ladder must read.
if (bytes != nullptr) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->hostBytes =
MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, start, end - start);
if (end > start) resource->contentSinceRespecify = true;
}
if (resource->pendingRespecify) return;
if (!CanTouchGLNow() || resource->id == 0 || resource->contextGeneration != g_bufferContextGeneration ||
!StorageMatchesSize(*resource, ResourceWidthOf(res))) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({start, end});
return;
}
if (resource->persistentMapped && resource->persistentPtr) {
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
if (!MG_Config::Features.EsprytDisableUploadRing) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({start, end});
return;
}
// The kill-switch arm, and it reads the application's REAL flags per call -
// which is why MGPFlushRange carries them unnormalised.
const Flags<BufferMappingAccessBit> appAccess{
static_cast<std::underlying_type_t<BufferMappingAccessBit>>(record.AccessFlags)};
const Bool invalidate = (appAccess & BufferMappingAccessBit::InvalidateRange) ||
(appAccess & BufferMappingAccessBit::InvalidateBuffer);
const Bool unsynchronized = static_cast<Bool>(appAccess & BufferMappingAccessBit::Unsynchronized);
if (PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC && (invalidate || unsynchronized) &&
resource->hostBytes != nullptr) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The kill-switch arm's own Memcpy, and the one that passes
// GL_MAP_INVALIDATE_RANGE_BIT - i.e. the one that tells the driver the old
// bytes are dead. Bytes outside the staged coverage are exactly the ones
// that claim is false for.
MGL_SERVER_STAGED_REQUIRE(*resource, resource->hostBytes, start, end, "flush_range_invalidate_map");
BindBufferId(TempBufferTarget, resource->id);
void* mappedData =
g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start),
GL_MAP_WRITE_BIT | (invalidate ? GL_MAP_INVALIDATE_RANGE_BIT : 0) |
(unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0));
if (mappedData) {
Memcpy(mappedData, resource->hostBytes + start, end - start);
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
MGLOG_E_ONCE("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData",
resource->id);
}
UploadRangeFrom(*resource, resource->hostBytes, start, end);
resource->syncedChangeSerial = ResourceSerialOf(res);
}
void Ops_H_Readback(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPReadback& record) {
// An empty capture may conservatively request a writeback before
// any draw materialized this buffer. Resolve its staged resource
// exactly as a GPU consumer would, then return the real bytes.
auto* resource = EnsureBufferResourceForHandle(nullptr, res);
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
if (resource->persistentMapped) {
// Queued resident SubData bytes land first (GPU-ordered), then the finish
// makes them - and any shader writes already queued on this context -
// visible through the coherent mapping the reads use. There is no backend
// copy to read back in this case.
DrainResidentWritesNow(*resource);
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
return;
}
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
// The map below starts at record.Offset where the legacy arm mapped at literal
// zero, so the clamp has to be against what is left of the BACKEND store past
// that offset: the applier's BufferRangeFault checked the range against
// Desc.Width, and a store that is smaller than the descriptor (a respecify the
// backend has not applied yet) would otherwise be mapped past its end.
const SizeT readOffset = static_cast<SizeT>(record.Offset);
if (readOffset >= resource->storageSize) return;
const SizeT size = std::min<SizeT>(static_cast<SizeT>(record.Size), resource->storageSize - readOffset);
if (size == 0) return;
// Queued app writes must land in the backend store before it is read back, or
// the writeback below would revert them in the shadow. Under split every queued
// range must lie inside the server shadow's staged coverage first (R-11 / the
// M-6 ruling): the drain's tier-1 map would otherwise declare live GPU bytes
// dead over a range nothing staged.
MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, resource->hostBytes, ResourceWidthOf(res),
"readback_flush_pending");
FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res));
BindBufferId(TempBufferTarget, resource->id);
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)record.Offset,
static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E_ONCE("Ops_H_Readback: glMapBufferRange(read) failed for buffer %u", resource->id);
return;
}
// The reverse channel replaces BufferObject::WritebackFromBackend: the backend
// no longer reaches into the frontend's address space, it ANSWERS. In monolith
// the client's implementation is one call away, so SyncGpuWrites' caller still
// sees the reconciled shadow on return, exactly as before.
//
// THE ORDER FROM HERE IS A CORRECTNESS RULE, NOT A PREFERENCE
// (ARCHITECTURE.md 8.2: "写回的 epoch bump 必须在任何后续读该 handle 的命令之前被
// server 应用;反向通道需要与正向通道相同的有序保证"):
// 1. writeback - the client's shadow takes the bytes,
// 2. unmap - the read mapping goes away,
// 3. serial stamp- syncedChangeSerial catches up with the record, so the next
// draw does not re-upload the readback over itself,
// 4. epoch bump - in Ops_H_ReadbackTracked, i.e. strictly AFTER 1-3, so a
// draw-clean memo re-probed by the bump can never observe a
// half-reconciled resource. The bump must NEVER move earlier.
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(res, record.Offset,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(mapped),
static_cast<Uint64>(size),
MG_Pipe::kMGHostSpanSegNone, 0});
} else {
MGLOG_E_ONCE("Ops_H_Readback: no reverse channel is installed, so the GPU-written bytes of "
"buffer %u cannot reach the client shadow",
resource->id);
}
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
// The shadow now matches the backend byte for byte; without this the next draw
// would see a newer serial and re-upload the readback over it.
resource->syncedChangeSerial = ResourceSerialOf(res);
// P13 W4a: on monolith's record arm the base this twin reads IS the shadow the
// writeback just filled, so a whole-store readback is also the refresh
// SplitHostBytesForCpuRead would owe. (A copying store's copy is not the
// client's shadow, and keeps its mark.)
if (MG_Config::RecordArmAliasesFrontend() && readOffset == 0 && size >= ResourceWidthOf(res)) {
resource->serverGpuWritten = false;
}
}
void Ops_H_Destroy(MG_Pipe::MGPipeHandle res) {
// R-11's server copy dies with the resource, and BEFORE the twin leaves the
// table - the side map is keyed by the twin's address, so this is the last
// moment that address can be looked up.
if (GLESBufferResource* dying = FindBufferResourceForHandle(res); dying != nullptr) {
MGL_SERVER_STAGED_DROP(*dying);
}
// The twin comes OUT of the table first, so the three outcomes below are
// reached with the entry already retired. The SLOT is the client's to free,
// after this returns (D-L).
SharedPtr<BackendBufferResource> twin = g_backendBufferResources.ReleaseByHandle(res);
// Verbatim Ops_OnDestroy: stale generation -> zero the id; on-thread ->
// IsPoolable/EnrollIntoPool else scrub + glDeleteBuffers; off-thread ->
// g_deferredBufferReleases plus the lock-free flag.
Ops_OnDestroy(std::move(twin));
}
// D-E: the ONLY two changes are the signature and the three reads that went
// through the frontend object (GetSize() -> size, MappedData() -> seedBytes,
// SetBackendResource(...) -> the slot table's GetOrCreate). Every other statement
// is byte-identical to Ops_AcquirePersistentMap above - the four-way capability
// gate, the stale-generation wipe BEFORE the new stamp, the idempotency hit, the
// fresh-id sequence with ++g_bufferBackendIdGeneration, immutableStorage set as
// soon as the store exists, the MGLOG_E_ONCE decline and the success stamps.
void* Ops_H_MapPersistent(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes) {
if (!CanTouchGLNow() || !g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange ||
!g_GLESFuncs.glGenBuffers) {
return nullptr;
}
if (size == 0) return nullptr;
auto* resource = GetOrCreateBufferResourceForHandle(res);
if (!resource) return nullptr;
// Before the generation is stamped, not after: everything on the resource
// describes a context that is gone, and the idempotency check below would
// otherwise hand the caller the dead context's mapped pointer.
if (resource->contextGeneration != g_bufferContextGeneration) {
resource->id = 0;
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
resource->immutableStorage = false;
resource->storageInitialized = false;
resource->storageSize = 0;
}
resource->contextGeneration = g_bufferContextGeneration;
if (resource->persistentMapped && resource->persistentPtr && resource->storageSize == size) {
return resource->persistentPtr; // idempotent
}
// 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) {
NoteBufferIdDeleted(resource->id);
// Driver VAOs may have this id baked into attribute/element bindings
// keyed on versions this re-mint does not move.
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
}
g_GLESFuncs.glGenBuffers(1, &resource->id);
if (resource->id == 0) return nullptr;
// Seed from the shadow (the client's bytes are still live at this point: it
// adopts and drops them only after this returns).
BindBufferId(TempBufferTarget, resource->id);
const void* initial = seedBytes;
g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast<GLsizeiptr>(size), initial,
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit |
kDynamicStorageBit);
// Set as soon as the store exists, not once the map succeeds: the failure
// path below leaves this id holding immutable storage, and whoever touches
// it next has to know that glBufferData cannot redefine it.
resource->immutableStorage = true;
void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
if (!ptr) {
MGLOG_E_ONCE("Ops_H_MapPersistent: glMapBufferRange(persistent) failed for buffer %u",
resource->id);
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
return nullptr;
}
resource->persistentPtr = ptr;
resource->persistentMapped = true;
// The ONE statement here that is not in Ops_AcquirePersistentMap, and it is
// about a member the legacy arm does not have rather than about the map: the
// client adopts this pointer the instant we return it and
// PipeResource::AdoptPersistentMap then does m_shadow->clear() +
// shrink_to_fit(), so the shadow base any earlier content-carrying call
// recorded is a FREED allocation from here on. The live bytes are persistentPtr.
// R-11's server copy goes with it: the bytes are the coherent map's now, and a
// stale server shadow would answer a later drain with pre-map content.
MGL_SERVER_STAGED_DROP(*resource);
resource->hostBytes = nullptr;
resource->storageSize = static_cast<SizeT>(size);
resource->storageInitialized = true;
resource->pendingRespecify = false;
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
}
resource->syncedChangeSerial = ResourceSerialOf(res);
return ptr;
}
void Ops_H_UnmapPersistent(MG_Pipe::MGPipeHandle res) {
(void)res;
// P3a emits this from nowhere: the donation is permanent for the life of the
// store and is ended by the respecify / destroy paths, which already retire
// the immutable id. The entry exists so the transport has both halves of the
// pair (D-E), and giving it a body that unmapped a live coherent store would
// be the one thing D-B4 forbids.
}
// The epoch-tracking wrappers, duplicated for this table - same contract as the
// seven above, including AcquirePersistentMap's bump EVEN ON DECLINE (the client
// still enters a persistent map the per-draw probes must start seeing).
void Ops_H_RespecifyTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
const void* initialBytes) {
Ops_H_Respecify(res, desc, initialBytes);
BumpBufferMutationEpoch();
}
void Ops_H_SubDataTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, const void* bytes) {
Ops_H_SubData(res, record, bytes);
BumpBufferMutationEpoch();
}
void Ops_H_ResidentSubDataTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record,
const void* bytes) {
Ops_H_ResidentSubData(res, record, bytes);
BumpBufferMutationEpoch();
}
void Ops_H_FlushRangeTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPFlushRange& record,
const void* bytes) {
Ops_H_FlushRange(res, record, bytes);
BumpBufferMutationEpoch();
}
void Ops_H_ReadbackTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPReadback& record) {
Ops_H_Readback(res, record);
BumpBufferMutationEpoch();
}
void Ops_H_DestroyTracked(MG_Pipe::MGPipeHandle res) {
Ops_H_Destroy(res);
BumpBufferMutationEpoch();
}
void* Ops_H_MapPersistentTracked(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes) {
void* result = Ops_H_MapPersistent(res, size, seedBytes);
// Bump even on decline: the client still enters a persistent map the per-draw
// probes must start seeing.
BumpBufferMutationEpoch();
return result;
}
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
// ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A STORE'S RESIDENT TWIN ------------
//
// The split twin of Ops_H_MapPersistent: where a monolith map_persistent mints a
// coherent persistent store (glBufferStorageEXT) and hands its pointer to the frontend,
// a T0 map_persistent IMPORTS the pages the client already holds and maps -
// glBufferStorageExternalEXT on eglGetNativeClientBufferANDROID(ahb) - and the result
// is the SAME resource shape (immutable, persistentMapped, persistentPtr = this
// server's coherent map of those pages), so every adopted-store path in this file
// applies unchanged: draws bind it with nothing to upload, resident SubData lands
// GPU-ordered (DrainResidentWritesNow), a readback drains and waits and posts no
// bytes. The R-11 staged copy is dropped: the pages are the client's.
//
// THE ENTRY POINTS ARE OPTIONAL: neither is in the loader's tables (an EGL function
// missing there is MGLOG_F), and eglGetProcAddress hands back live-looking stubs, so
// the extension strings decide and the pointers are only then taken.
using T0GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*);
using T0BufferStorageExternalFn = void (*)(GLenum, GLintptr, GLsizeiptr, EGLClientBuffer, GLbitfield);
T0GetNativeClientBufferFn g_t0GetNativeClientBuffer = nullptr;
T0BufferStorageExternalFn g_t0BufferStorageExternal = nullptr;
constexpr GLbitfield kT0MapReadBit = 0x0001;
constexpr GLbitfield kT0MapWriteBit = 0x0002;
constexpr GLbitfield kT0StorageFlags =
kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit | kDynamicStorageBit;
constexpr GLbitfield kT0MapFlags = kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit;
Bool ResolveT0EntryPoints(String& why) {
if (g_t0GetNativeClientBuffer != nullptr && g_t0BufferStorageExternal != nullptr) return true;
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay ||
!g_GLESFuncs.glGetString || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers) {
why = "the loader has no eglGetProcAddress/eglQueryString/glGetString/glMapBufferRange";
return false;
}
const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS);
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
why = "EGL_ANDROID_get_native_client_buffer is not advertised";
return false;
}
if (!HasExtensionWord(gl, "GL_EXT_external_buffer") || !HasExtensionWord(gl, "GL_EXT_buffer_storage")) {
why = "GL_EXT_external_buffer / GL_EXT_buffer_storage is not advertised";
return false;
}
g_t0GetNativeClientBuffer = reinterpret_cast<T0GetNativeClientBufferFn>(
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
g_t0BufferStorageExternal = reinterpret_cast<T0BufferStorageExternalFn>(
g_EGLFuncs.eglGetProcAddress("glBufferStorageExternalEXT"));
if (g_t0GetNativeClientBuffer == nullptr || g_t0BufferStorageExternal == nullptr) {
why = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / glBufferStorageExternalEXT";
g_t0GetNativeClientBuffer = nullptr;
g_t0BufferStorageExternal = nullptr;
return false;
}
return true;
}
void DrainGLErrors() {
if (!g_GLESFuncs.glGetError) return;
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {}
}
// A fresh GL buffer whose storage IS `ahb`, mapped persistent + coherent. 0 on failure
// (with `why`); nothing on the resource is touched here. `throwaway` = the POST's own
// context is current: bind raw and keep Espryt's binding caches (which describe the
// session's context, whose name space this is not) out of it.
Uint ImportAhbAsBuffer(void* ahb, Uint64 size, void** outPtr, String& why, Bool throwaway = false) {
*outPtr = nullptr;
const EGLClientBuffer clientBuffer =
g_t0GetNativeClientBuffer(static_cast<const struct AHardwareBuffer*>(ahb));
if (clientBuffer == nullptr) {
why = "eglGetNativeClientBufferANDROID returned null";
return 0;
}
Uint id = 0;
g_GLESFuncs.glGenBuffers(1, &id);
if (id == 0) {
why = "glGenBuffers gave no name";
return 0;
}
DrainGLErrors();
if (throwaway)
g_GLESFuncs.glBindBuffer(TempBufferTarget, id);
else
BindBufferId(TempBufferTarget, id);
g_t0BufferStorageExternal(TempBufferTarget, 0, static_cast<GLsizeiptr>(size), clientBuffer,
kT0StorageFlags);
const GLenum stored = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR;
void* ptr =
stored == GL_NO_ERROR
? g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size), kT0MapFlags)
: nullptr;
if (ptr == nullptr) {
char text[160];
std::snprintf(text, sizeof(text), "glBufferStorageExternalEXT -> 0x%04x, map %s", stored,
stored == GL_NO_ERROR ? "failed" : "not attempted");
why = text;
if (!throwaway) {
NoteBufferIdDeleted(id);
++g_bufferBackendIdGeneration;
}
g_GLESFuncs.glDeleteBuffers(1, &id);
return 0;
}
*outPtr = ptr;
return id;
}
Bool T0Answer(char* out, Uint64 outBytes, Bool ok, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
std::vsnprintf(out, static_cast<SizeT>(outBytes), fmt, args);
va_end(args);
return ok;
}
// Installs an imported store on the twin. The caller has retired whatever was there.
void InstallT0Store(GLESBufferResource& resource, Uint id, void* ptr, Uint64 size) {
resource.id = id;
resource.immutableStorage = true;
resource.persistentPtr = ptr;
resource.persistentMapped = true;
resource.storageSize = static_cast<SizeT>(size);
resource.storageInitialized = true;
resource.pendingRespecify = false;
resource.contextGeneration = g_bufferContextGeneration;
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
resource.pendingRanges.clear();
resource.pendingResidentWrites.clear();
}
Bool Ops_H_ImportExternal(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
SweepT0Retired(false);
if (!CanTouchGLNow() || size == 0 || ahb == nullptr) return false;
String why;
if (!ResolveT0EntryPoints(why)) {
MGLOG_W_ONCE("Espryt T0: no import on this context - %s", why.c_str());
return false;
}
auto* resource = GetOrCreateBufferResourceForHandle(res);
if (resource == nullptr) return false;
// IMPORT FIRST, retire second: a refused import leaves the T2 store (and its staged
// copy) exactly as it was, so the DECLINED the client gets leaves nothing to repair.
void* ptr = nullptr;
const Uint id = ImportAhbAsBuffer(ahb, size, &ptr, why);
if (id == 0) {
MGLOG_W("Espryt T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen,
static_cast<unsigned long long>(size), why.c_str());
return false;
}
if (resource->contextGeneration == g_bufferContextGeneration && resource->id != 0) {
NoteBufferIdDeleted(resource->id);
// Driver VAOs may have the old id baked into bindings keyed on versions this
// replacement does not move (the Ops_H_MapPersistent re-mint's rule).
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
}
resource->id = 0;
RetireT0Import(*resource); // a previous import of this handle, fenced
InstallT0Store(*resource, id, ptr, size);
MG_Remote::Transport::AdoptT0::AcquireImported(ahb);
resource->externalAhb = ahb;
++g_t0ImportsLive;
// The bytes are the client's pages now; R-11's copy would answer a later drain
// with pre-import content.
MGL_SERVER_STAGED_DROP(*resource);
resource->hostBytes = nullptr;
resource->syncedChangeSerial = ResourceSerialOf(res);
MGLOG_D("Espryt T0: imported {slot=%u, gen=%u} as buffer %u (%llu bytes); %llu live imports", res.Slot,
res.Gen, id, static_cast<unsigned long long>(size),
static_cast<unsigned long long>(g_t0ImportsLive));
return true;
}
Bool Ops_H_ImportExternalTracked(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
const Bool imported = Ops_H_ImportExternal(res, ahb, size);
BumpBufferMutationEpoch();
return imported;
}
// The POST's second round: the CPU writes region 1 through the held lock only now, after
// the import and a GPU use of it, and reads region 4 (the first round's fill) back into
// its cache; a second dispatch copies region 1 out and overwrites region 4. Appends its
// counts to `detail`.
Bool SelfTestExternalSteady(GLuint imported, GLuint out, void* heldPtr, char* detail, SizeT detailBytes) {
constexpr Uint32 kWords = 1024;
constexpr Uint32 kSteadyAtWord = kWords; // region 1
constexpr Uint32 kFillAtWord = 4 * kWords; // region 4
auto* held = static_cast<volatile Uint32*>(heldPtr);
Uint32 cached = 0;
for (Uint32 i = 0; i < kWords; ++i) {
held[kSteadyAtWord + i] = 0xA5000000u ^ (i * 40503u);
cached ^= held[kFillAtWord + i];
}
(void)cached;
static const char* kSource = "#version 310 es\n"
"layout(local_size_x = 64) in;\n"
"layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n"
"layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n"
"void main() {\n"
" uint i = gl_GlobalInvocationID.x;\n"
" outb.w[i] = ext.w[1024u + i];\n"
" ext.w[4096u + i] = 0x3D5EA11Eu ^ i;\n"
"}\n";
const GLuint program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource);
GLint linked = 0;
if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked);
const SizeT used = std::strlen(detail);
if (!linked) {
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
std::snprintf(detail + used, detailBytes - used, "; steady: the second program did not link");
return false;
}
g_GLESFuncs.glUseProgram(program);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out);
g_GLESFuncs.glMemoryBarrier(0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1);
g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT |
0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
g_GLESFuncs.glFinish();
Uint32 steadyRead = 0, steadySaw = 0;
for (Uint32 i = 0; i < kWords; ++i)
if (held[kFillAtWord + i] == (0x3D5EA11Eu ^ i)) ++steadySaw;
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
const auto* copied = static_cast<const Uint32*>(
g_GLESFuncs.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT));
if (copied != nullptr) {
for (Uint32 i = 0; i < kWords; ++i)
if (copied[i] == (0xA5000000u ^ (i * 40503u))) ++steadyRead;
g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
}
g_GLESFuncs.glDeleteProgram(program);
std::snprintf(detail + used, detailBytes - used,
"; with the import in use, GPU read %u/%u words the CPU wrote after it, GPU write "
"seen through the held lock %u/%u words",
steadyRead, kWords, steadySaw, kWords);
return steadyRead == kWords && steadySaw == kWords;
}
// THE POST (B2, the standing rule: decide by probe, never by driver name). The AHB API
// promises CPU coherence only across an unlock/lock; T0 keeps the client's lock for the
// store's life, so the pattern it depends on is exactly what this exercises, on this
// context's own driver, before kCapAdoptT0 may be published:
// hold - a 64 KiB BLOB allocated and locked by this test, never unlocked while it runs;
// read - this server's own host map of the import reads the pattern the CPU wrote
// through the held pointer (every hostBytes fallback reads persistentPtr);
// gpu - a compute dispatch copies that pattern out of the import (the GPU read) and
// fills another region of it (the GPU write);
// back - after glFinish the fill is read through the HELD pointer and the copy is
// read back from the dispatch's output;
// steady - the same again with bytes the CPU wrote AFTER the import and its first GPU
// use (VkBufferManager::SelfTestWireImport says why the first round is not enough).
// In a throwaway ES 3.1 pbuffer context, so none of this context's state or Espryt's
// binding caches is disturbed; the caller's bind is restored before returning.
Bool Ops_H_SelfTestExternal(char* whyOut, Uint64 whyBytes) {
const auto answer = [&](Bool ok, const char* text, const char* arg = "") {
return T0Answer(whyOut, whyBytes, ok, text, arg);
};
String why;
if (!ResolveT0EntryPoints(why)) return answer(false, "%s", why.c_str());
if (!g_EGLFuncs.eglChooseConfig || !g_EGLFuncs.eglCreateContext || !g_EGLFuncs.eglMakeCurrent ||
!g_EGLFuncs.eglCreatePbufferSurface || !g_GLESFuncs.glCreateShaderProgramv ||
!g_GLESFuncs.glDispatchCompute || !g_GLESFuncs.glMemoryBarrier || !g_GLESFuncs.glFinish)
return answer(false, "the loader lacks the EGL/GLES 3.1 entry points the test needs");
const EGLDisplay display = g_EGLFuncs.eglGetCurrentDisplay();
const EGLContext savedContext = g_EGLFuncs.eglGetCurrentContext();
const EGLSurface savedDraw = g_EGLFuncs.eglGetCurrentSurface(EGL_DRAW);
const EGLSurface savedRead = g_EGLFuncs.eglGetCurrentSurface(EGL_READ);
const EGLint configAttribs[] = {EGL_RENDERABLE_TYPE, 0x0040 /*EGL_OPENGL_ES3_BIT*/, EGL_SURFACE_TYPE,
EGL_PBUFFER_BIT, EGL_NONE};
EGLConfig config = nullptr;
EGLint configs = 0;
if (!g_EGLFuncs.eglChooseConfig(display, configAttribs, &config, 1, &configs) || configs < 1)
return answer(false, "no ES3 pbuffer config for the throwaway context");
const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(display, config, surfaceAttribs);
const EGLint contextAttribs[] = {0x3098 /*EGL_CONTEXT_MAJOR_VERSION*/, 3, 0x30FB /*MINOR*/, 1,
EGL_NONE};
const EGLContext context =
surface != EGL_NO_SURFACE
? g_EGLFuncs.eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs)
: EGL_NO_CONTEXT;
const auto restore = [&] {
g_EGLFuncs.eglMakeCurrent(display, savedDraw, savedRead, savedContext);
if (context != EGL_NO_CONTEXT) g_EGLFuncs.eglDestroyContext(display, context);
if (surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(display, surface);
};
if (context == EGL_NO_CONTEXT || !g_EGLFuncs.eglMakeCurrent(display, surface, surface, context)) {
restore();
return answer(false, "the throwaway ES 3.1 context could not be made current");
}
constexpr Uint64 kBytes = 64 * 1024;
constexpr Uint32 kWords = 1024; // one 4 KiB region
constexpr Uint32 kFillAtWord = 4 * kWords; // region 4
MG_Remote::Transport::AdoptT0::HeldStore held;
Vector<Uint32> pattern(kBytes / 4);
for (SizeT i = 0; i < pattern.size(); ++i)
pattern[i] = 0x5A000000u ^ static_cast<Uint32>(i * 2654435761u);
if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why)) {
restore();
return answer(false, "hold: %s", why.c_str());
}
Bool ok = false;
GLuint out = 0, program = 0;
void* hostMap = nullptr;
const GLuint imported = ImportAhbAsBuffer(held.ahb, kBytes, &hostMap, why, /*throwaway=*/true);
char detail[512] = {};
if (imported == 0) {
std::snprintf(detail, sizeof(detail), "import: %s", why.c_str());
} else if (std::memcmp(hostMap, pattern.data(), kWords * 4) != 0) {
std::snprintf(detail, sizeof(detail),
"read: the server's host map of the import does not "
"hold the CPU's pattern");
} else {
static const char* kSource = "#version 310 es\n"
"layout(local_size_x = 64) in;\n"
"layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n"
"layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n"
"void main() {\n"
" uint i = gl_GlobalInvocationID.x;\n"
" outb.w[i] = ext.w[i];\n"
" ext.w[4096u + i] = 0x6C0FFEE0u ^ i;\n"
"}\n";
program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource);
GLint linked = 0;
if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked);
g_GLESFuncs.glGenBuffers(1, &out);
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
g_GLESFuncs.glBufferData(GL_SHADER_STORAGE_BUFFER, kWords * 4, nullptr, GL_DYNAMIC_READ);
if (!linked) {
std::snprintf(detail, sizeof(detail), "gpu: the test's compute program did not link");
} else {
g_GLESFuncs.glUseProgram(program);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out);
g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1);
g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT |
0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
g_GLESFuncs.glFinish();
Uint32 gpuRead = 0, heldSaw = 0;
const auto* heldWords = static_cast<const Uint32*>(held.ptr);
for (Uint32 i = 0; i < kWords; ++i)
if (heldWords[kFillAtWord + i] == (0x6C0FFEE0u ^ i)) ++heldSaw;
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
const auto* copied = static_cast<const Uint32*>(
g_GLESFuncs.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT));
if (copied != nullptr) {
for (Uint32 i = 0; i < kWords; ++i)
if (copied[i] == pattern[i]) ++gpuRead;
g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
}
ok = gpuRead == kWords && heldSaw == kWords;
std::snprintf(detail, sizeof(detail),
"64 KiB AHB, lock held throughout: host map read ok, GPU read %u/%u words of "
"the CPU's pattern, GPU write seen through the held lock %u/%u words",
gpuRead, kWords, heldSaw, kWords);
if (ok) ok = SelfTestExternalSteady(imported, out, held.ptr, detail, sizeof(detail));
}
}
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
if (out != 0) g_GLESFuncs.glDeleteBuffers(1, &out);
if (imported != 0) g_GLESFuncs.glDeleteBuffers(1, &imported);
g_GLESFuncs.glFinish();
restore();
MG_Remote::Transport::AdoptT0::ReleaseHeld(held);
return answer(ok, "%s", detail);
}
#endif // MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__
} // namespace
// ---- P5c (tx): the TEXTURE half of the resource family, staged server-side --------
//
// The buffer half's R-11 pattern (ServerStaged() above), for texture levels: the
// staged bytes of resource_subdata's texture half are adopted into the server's
// StagedTextureStore AT APPLY TIME, because `bytes` names SEG_STAGE and is dead the
// moment the record retires (rule C). The store, its ownership and its coverage
// rules are documented in MG_Remote/Server/StagedTextureStore.h; these are only the
// three hook bodies. All three are no-ops in monolith (CopiesIntoServerStorage()
// false), which is what keeps the monolith expression character for character.
//
// NOTHING here mints or reads a texture twin: the store is keyed by the wire handle
// the record carried (StagedTextureStore.h explains why not the twin address), so
// adoption needs no GL call and no frontend object.
// GetUploadTargets() answered from the descriptor's Target: one static list per
// target, matching the frontend classes' own lists member for member
// (TextureObject*.h / TextureObjectStubs.h: a cube is six faces, a cube array is
// the single CubeMapArray target, everything else its own one).
const Vector<TextureUploadTarget>& StagedUploadTargetsForPipeTarget(Uint8 pipeResourceTarget) {
static const Vector<TextureUploadTarget> kUnknown{};
static const Vector<TextureUploadTarget> kTex1D{TextureUploadTarget::Texture1D};
static const Vector<TextureUploadTarget> kTex2D{TextureUploadTarget::Texture2D};
static const Vector<TextureUploadTarget> kTex3D{TextureUploadTarget::Texture3D};
static const Vector<TextureUploadTarget> kTex1DArray{TextureUploadTarget::Texture1DArray};
static const Vector<TextureUploadTarget> kTex2DArray{TextureUploadTarget::Texture2DArray};
static const Vector<TextureUploadTarget> kTexCube{
TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX,
TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY,
TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ};
static const Vector<TextureUploadTarget> kTexCubeArray{TextureUploadTarget::CubeMapArray};
static const Vector<TextureUploadTarget> kTex2DMS{TextureUploadTarget::Texture2DMultisample};
static const Vector<TextureUploadTarget> kTex2DMSArray{TextureUploadTarget::Texture2DMultisampleArray};
static const Vector<TextureUploadTarget> kTexRect{TextureUploadTarget::TextureRectangle};
static const Vector<TextureUploadTarget> kTexBuffer{TextureUploadTarget::TextureBuffer};
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1D:
return kTex1D;
case MG_Pipe::MGPipeResourceTarget::Tex2D:
return kTex2D;
case MG_Pipe::MGPipeResourceTarget::Tex3D:
return kTex3D;
case MG_Pipe::MGPipeResourceTarget::Tex1DArray:
return kTex1DArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray:
return kTex2DArray;
case MG_Pipe::MGPipeResourceTarget::TexCube:
return kTexCube;
case MG_Pipe::MGPipeResourceTarget::TexCubeArray:
return kTexCubeArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DMS:
return kTex2DMS;
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray:
return kTex2DMSArray;
case MG_Pipe::MGPipeResourceTarget::TexRect:
return kTexRect;
case MG_Pipe::MGPipeResourceTarget::TexBuffer:
return kTexBuffer;
default:
return kUnknown;
}
}
// P5e (ID-124): OUT OF THE DISAGGREGATED BLOCK ON PURPOSE. This is a pure
// MGPipeResourceTarget -> TextureTarget switch with no wire, no record and no role in it, but
// it sat inside the nested `#if MOBILEGL_BUILD_DISAGGREGATED` below while its only caller,
// MGB_TEXPARAM_TARGET, is guarded by `#if MOBILEGL_PIPE_PUSH` alone - so a push build without
// disaggregation did not compile. The narrower fix (guarding the macro block too) is wrong:
// that block also carries TextureDiagName and MGB_TEXTURE_RECORD_ARM_SELECTED, which the push
// flavour does need. Caught by the `flavours` gate step; see ID-124.
// The inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the sync's
// target reads (ConvertTextureTargetToBackendGLEnum and MapToBackendTextureTarget
// both want the frontend enum).
TextureTarget StagedTextureTargetForPipeTarget(Uint8 pipeResourceTarget) {
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1D:
return TextureTarget::Texture1D;
case MG_Pipe::MGPipeResourceTarget::Tex2D:
return TextureTarget::Texture2D;
case MG_Pipe::MGPipeResourceTarget::Tex3D:
return TextureTarget::Texture3D;
case MG_Pipe::MGPipeResourceTarget::Tex1DArray:
return TextureTarget::Texture1DArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray:
return TextureTarget::Texture2DArray;
case MG_Pipe::MGPipeResourceTarget::TexCube:
return TextureTarget::TextureCubeMap;
case MG_Pipe::MGPipeResourceTarget::TexCubeArray:
return TextureTarget::TextureCubeMapArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DMS:
return TextureTarget::Texture2DMultisample;
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray:
return TextureTarget::Texture2DMultisampleArray;
case MG_Pipe::MGPipeResourceTarget::TexRect:
return TextureTarget::TextureRectangle;
case MG_Pipe::MGPipeResourceTarget::TexBuffer:
return TextureTarget::TextureBuffer;
default:
return TextureTarget::Unknown;
}
}
namespace {
void Ops_H_TextureSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, const void* bytes,
const MG_Pipe::MGPSubRegion* regions) {
// The region set is the upload planner's shape and stays in the applier's
// pending set; the store's coverage is the staged run itself
// (StagedTextureStore.h's coverage ruling). It is ALSO what places that run in
// the level image when the record carries a piece of the level rather than the
// whole of it (fix A2), which is why it is read here now.
auto& store = MG_Record::ServerStagedTexture();
// P13 W4b: an aliasing store (monolith's record arm) adopts too - it records where
// the run sits in the frontend's level shadow instead of copying it.
if (!store.Holds()) return;
// The applier's gate has already faulted every shape that reaches here without
// bytes, and under split the codec declared the run's length (Blob.Size) -
// TextureEmit.h's "the bytes this record declares ARE the level shadow".
if (bytes == nullptr || record.Blob.Size == 0) return;
const auto* stored = PipeTextureRecordForHandle(res);
if (stored == nullptr) return;
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res);
const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target);
const IntVec3 extent = MG_Record::StagedTextureUploadExtent(stored->Desc, record);
const SizeT runBytes = static_cast<SizeT>(record.Blob.Size);
// P8-E (docs/archive/notes/p8/E.md): THE LEVEL HOLDS A COPY THE CLIENT NEVER SAW
// (FollowCopyImageInStagedStore). The run is the client's shadow, stale everywhere it
// did not write, so only the texels it names land - its regions, or its union box
// when it carries none (D-D3's "the box is the whole story"). A run the store cannot
// place that way is adopted as ever, and the store stops standing for the texture.
if (store.LevelHoldsFollowedCopy(key, uploadTarget, record.Level) &&
(record.RegionCount == 0 || regions != nullptr)) {
Vector<MG_Record::StagedTextureStore::ClientTexelBox> boxes;
if (record.RegionCount == 0) {
const auto& box = record.UnionBox;
boxes.push_back({box.X, box.Y, box.Z, box.W, box.H, box.D});
} else {
for (Uint32 i = 0; i < record.RegionCount; ++i) {
const auto& region = regions[i];
boxes.push_back({region.X, region.Y, region.Z, region.W, region.H, region.D});
}
}
const Uint64 imageOffset =
record.RegionCount == 0 ? 0 : MG_Record::StagedTextureRunImageOffset(record, regions);
if (store.AdoptClientBoxes(key, uploadTarget, record.Level, extent, imageOffset, bytes, runBytes,
boxes.data(), boxes.size())) {
return;
}
store.MarkDriverWritten(key);
}
// RegionCount == 0 IS the whole-level spelling - "the run is the level shadow",
// beginning at the level's first byte - so it is adopted with the spelling that
// replaces the level; every other record names a RUN of the level by its own box.
// A client record's run: under monolith's record arm it IS the frontend level shadow,
// the one source an aliasing store keeps by address.
if (record.RegionCount == 0) {
store.Adopt(key, uploadTarget, record.Level, extent, bytes, runBytes, /*frontendShadow=*/true);
return;
}
store.AdoptRun(key, uploadTarget, record.Level, extent,
MG_Record::StagedTextureRunImageOffset(record, regions), bytes, runBytes,
/*frontendShadow=*/true);
}
void Ops_H_TextureRespecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
const MG_Pipe::MGPRespecifiedLevel* level) {
auto& store = MG_Record::ServerStagedTexture();
if (!store.Holds()) return;
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res);
if (level != nullptr) {
// ONE glTexImage*D redefined one level: its exact extent crossed with the
// accepted scope carrier, including a legal noncanonical mutable mip.
store.NoteLevelDefined(key, MG_Pipe::MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level,
IntVec3{static_cast<Int>(level->Width), static_cast<Int>(level->Height),
static_cast<Int>(level->Depth)},
desc.Target, desc.InternalFormat);
return;
}
// A whole-resource redefinition: every level's old coordinate system is gone.
// glTexStorage* then defines the WHOLE chain at once (GL 4.6 core 8.19 - all six
// cube faces included), so an immutable descriptor re-marks every level of every
// upload target; a mutable whole-resource respecify (a texture view) defines
// nothing here and the store simply forgets the old levels.
store.ResetLevels(key);
if (desc.Immutable == 0 || desc.Levels == 0) return;
for (const auto& uploadTarget : StagedUploadTargetsForPipeTarget(desc.Target)) {
for (Uint32 levelIndex = 0; levelIndex < desc.Levels; ++levelIndex) {
store.NoteLevelDefined(key, static_cast<Uint16>(uploadTarget), static_cast<Uint16>(levelIndex),
MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, levelIndex),
desc.Target, desc.InternalFormat);
}
}
}
void Ops_H_TextureDestroy(MG_Pipe::MGPipeHandle res) {
// Deliberately NOT gated on CopiesIntoServerStorage(): Drop's own m_any gate
// makes the monolith call one acquire load, and an unconditional drop cannot
// strand a key the latch state was misread for.
MG_Record::ServerStagedTexture().Drop(MG_Record::StagedTextureStore::KeyForHandle(res));
}
const MG_Pipe::MGPipeResourceOps g_glesResourceOps = {
.Create = Ops_H_Create,
.Respecify = Ops_H_RespecifyTracked,
.SubData = Ops_H_SubDataTracked,
.SubDataResident = Ops_H_ResidentSubDataTracked,
.FlushRange = Ops_H_FlushRangeTracked,
.Readback = Ops_H_ReadbackTracked,
.Destroy = Ops_H_DestroyTracked,
.MapPersistent = Ops_H_MapPersistentTracked,
.UnmapPersistent = Ops_H_UnmapPersistent,
.TextureSubData = Ops_H_TextureSubData,
.TextureRespecify = Ops_H_TextureRespecify,
.TextureDestroy = Ops_H_TextureDestroy,
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
// P11 B2 (T0). Android only: nowhere else is there an AHardwareBuffer to import.
.ImportExternal = Ops_H_ImportExternalTracked,
.SelfTestExternal = Ops_H_SelfTestExternal,
#endif
};
} // namespace
Uint64 CurrentBufferMutationEpoch() {
return g_bufferMutationEpoch.load(std::memory_order_acquire);
}
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
// P5e (vi): how many times BumpBufferMutationEpoch ran off the apply thread while
// one was running, with a live transport. See the note in the bump itself; the
// accessor is what a test can assert zero on instead of reading a call graph.
std::atomic<Uint64> g_bufferMutationEpochBumpsOffTheApplyThread{0};
} // namespace
Uint64 BufferMutationEpochBumpsOffTheApplyThread() {
return g_bufferMutationEpochBumpsOffTheApplyThread.load(std::memory_order_relaxed);
}
#endif
void BumpBufferMutationEpoch() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (vi), CONTRACT-P5E §5.1's first pin (scout S1 R2, BRIEF-P5E's open question 2):
// "does any GL-THREAD path bump this under a transport". PINNED HERE RATHER THAN
// ARGUED, because the argument is the kind that is true until someone adds a caller.
//
// It matters because this counter is the ONE non-serial input left in the record
// arm's clean gate: SyncVaoAttributeBuffersByRecord stamps the pre-pass value and
// skips every IsBufferDrawCleanByHandle probe while the stamp holds. A bump from the
// GL thread would make that stamp a cross-thread read of a value the applier never
// observed - the memo would go on reading clean over buffers the client had just
// dirtied - and under run-ahead the client is a frame ahead, so the window is a
// frame wide rather than a record wide.
//
// THE MEASURED ANSWER, and it is not the clean "no" the scout expected: there is
// EXACTLY ONE class of non-apply-thread bump under a transport, and it happens
// BEFORE THERE IS AN APPLY THREAD AT ALL. BackendObject_DirectGLES::Initialize()
// runs on the app thread by design ("the backend object is BUILT on the app thread
// while its context is never OWNED by it", ServerLoop.h) and calls
// RegisterBufferBackendOps(), whose last act is a bump. An unguarded refusal aborts
// the whole split lane in EGL bring-up, which is how this was found rather than
// reasoned.
//
// That class is harmless and the apply-thread key tells it apart from the one that
// would not be: with g_applyThreadKey == 0 no record has been applied, so no twin
// exists, so no memo holds a stamp for the bump to slide under.
//
// WHAT IS COUNTED AND WHAT IS FATAL, and why they are not the same thing. Every
// bump taken off the apply thread WHILE ONE IS RUNNING is counted and named once,
// and is Fatal UNDER THE STRICT LANE - which is where this phase's pins live, and
// which is the lane the answer is claimed for. It is not Fatal by default because
// the unit fixtures that drive the production op tables directly
// (StagedShadowProductionTest / ServerLoopTest, which register the real tables and
// call them from the test thread beside a started loop) are the server role without
// being the apply thread, and taking those down would be this pin policing a
// fixture rather than the draw path.
const Uint64 applyThreadKey = MG_Remote::Server::Detail::g_applyThreadKey.load(std::memory_order_relaxed);
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && applyThreadKey != 0 &&
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
g_bufferMutationEpochBumpsOffTheApplyThread.fetch_add(1, std::memory_order_relaxed);
if (MG_Config::Ipc.StrictErrors) {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"CurrentBufferMutationEpoch\"} - the "
"buffer-mutation epoch was bumped off the apply thread while an apply "
"thread was running, with a live transport. The draw-path memos stamp "
"this counter and skip their clean probes while the stamp holds "
"(CONTRACT-P5E §5.1), so a producer on the client's thread means the "
"backend is reachable from a thread that owns none of this state");
std::abort();
}
MGLOG_E_ONCE("MGPipe: the buffer-mutation epoch was bumped off the apply thread "
"with a live transport (CONTRACT-P5E §5.1's pin). Run the strict lane "
"to make this fatal at the producer");
}
#endif
g_bufferMutationEpoch.fetch_add(1, std::memory_order_release);
}
// The seventh slot table. A process-lifetime global, like the other six, so it links
// itself into its own holder list from its own constructor with no initialisation
// order question to answer (SlotTables.h).
BackendBufferResourceTable g_backendBufferResources;
GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res) {
if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr;
// The table refuses both of these itself; this is the release-build VOICE for the
// refusal, because MOBILEGL_ASSERT compiles out at INFO and a resource that silently
// stops being twinned is the failure mode the refusal exists to replace.
if (res.Slot >= BackendBufferResourceTable::kMaxHandleSlot) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
"buffer twin request named slot %u, past this table's %u bound",
res.Slot, BackendBufferResourceTable::kMaxHandleSlot);
#else
MGLOG_E_ONCE("MGPipe: resource handle slot %u is past the backend table's %u bound - "
"refusing to twin it",
res.Slot, BackendBufferResourceTable::kMaxHandleSlot);
return nullptr;
#endif
}
const Uint32 liveGen = g_backendBufferResources.LiveGenAt(res.Slot);
if (liveGen != 0 && liveGen > res.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
"buffer twin request named generation %u at slot %u, behind live generation %u",
res.Gen, res.Slot, liveGen);
#else
MGLOG_E_ONCE("MGPipe: resource handle {%u, %u} names a generation BEHIND the live twin's "
"%u - refusing rather than dropping the incumbent's driver storage",
res.Slot, res.Gen, liveGen);
return nullptr;
#endif
}
auto& twin = g_backendBufferResources.GetOrCreate(res);
if (!twin) {
twin = MakeShared<GLESBufferResource>();
// Same seed the lazy legacy path gives a resource it has just minted: nothing
// has defined storage yet, so the first sync owes a full (re)specification.
twin->pendingRespecify = true;
}
return twin.get();
}
GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res) {
auto* twin = g_backendBufferResources.FindByHandle(res);
return twin ? twin->get() : nullptr;
}
void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, SizeT end,
const char* site) {
ServerStaged().RequireCoverage(&resource, hostBase, start, end, site);
}
// M-3's rule for the WHOLE-STORE readers (v1 round 3). The descriptor is the
// application's own statement about the store: HasDefinedContent set means it SUPPLIED
// the content (glBufferData(size, data)), which under split arrives as resource_subdata
// records behind the respecify (table 1 row 19) - so a coverage gap at the draw is a
// MISSING RECORD and the zero-fill past the coverage is not the application's bytes.
// Clear means it ORPHANED the store (glBufferData(size, NULL), glBufferStorage(NULL)):
// every byte it has not staged since is UNDEFINED by its own declaration, the streaming
// idiom (orphan, partial glBufferSubData, draw) is the ordinary case, and uploading the
// shadow's zero-fill for the rest is exactly what the monolith arm uploads from
// MappedData(). Round 2 refused both shapes and aborted six LargeArenaAdoption /
// ResourceSubsystemControl entries on the joint by name (v1-v3.md 6).
//
// AT NAMESPACE SCOPE, not in the anonymous namespace it was written in (P3b/P4b espryt
// D1 slice 1): the XFB scatter guards ITS whole-store read with the same question, and
// a second spelling of "did the application declare these bytes" is how the pool-reuse
// ladder and the scatter end up disagreeing about one buffer.
Bool ResourceContentIsDeclared(MG_Pipe::MGPipeHandle res) {
const auto* record = ResourceRecordOf(res);
return record != nullptr && record->Desc.HasDefinedContent != 0;
}
void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes,
SizeT offset, SizeT size) {
if (bytes == nullptr || size == 0 || resource.externalAhb != nullptr) return;
// Only a COPYING store may take bytes that die with a mapping: the monolith one keeps
// the caller's pointer (StagedShadow.h), which here would dangle after the unmap.
if (!ServerStaged().CopiesIntoServerStorage()) return;
// Under pendingMutex, as every other hostBytes store is (M-2).
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, ResourceWidthOf(res), bytes, offset, size);
}
SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res) {
return ResourceWidthOf(res);
}
Uint64 ResourceSerialForHandle(MG_Pipe::MGPipeHandle res) {
return ResourceSerialOf(res);
}
Uint CurrentBufferContextGeneration() {
return g_bufferContextGeneration;
}
// See the declaration: re-mints of a live resource's driver id. Written only on
// the context thread (all re-mint sites run there), read only by the VAO sync.
Uint64 g_bufferBackendIdGeneration = 0;
void RegisterBufferBackendOps() {
// P3a: the handle-shaped resource table (P13: the only one - the legacy
// BufferBackendOps table is deleted). Registration is UNCONDITIONAL: the subsystem bit is the FRONTEND's
// dispatch predicate (MGPipeResourceSubsystemEnabled reads the bit AND this table's presence), so a build
// with bit 7 clear registers a table nobody calls and the A/B stays a pure configuration question rather
// than a bring-up-order one.
MG_Pipe::MGPipeSetResourceOps(&g_glesResourceOps);
// Frontend writes issued while ops were unregistered advanced change
// serials with no per-op bump; re-open every draw-clean memo. Advanced directly,
// past BumpBufferMutationEpoch's apply-thread pin: a registration mutates no buffer
// and only makes every memo probe again, and with several sessions in one process
// a new session's backend registers on its own thread while another session's
// apply thread is already running - which the pin would otherwise read as a
// client-thread producer and, under the strict lane, abort the server for.
g_bufferMutationEpoch.fetch_add(1, std::memory_order_release);
}
void UnregisterBufferBackendOps() {
if (MG_Pipe::MGPipeGetResourceOps() == &g_glesResourceOps) {
MG_Pipe::MGPipeSetResourceOps(nullptr);
}
// From here on frontend writes bypass the tracked ops entirely.
BumpBufferMutationEpoch();
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();
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
}
void OnServerSessionEndedWithOthersLive() {
// Only what belongs to the ending session: its staged bytes (both stores are
// session-scoped). The rings, the ops table and the buffer generation are the
// process's and every other session's GL buffers are still alive in the shared group.
MGL_SERVER_STAGED_DROP_ALL();
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Record::ServerStagedTexture().DropAll();
#endif
}
void OnBackendContextDestroyed() {
UnregisterBufferBackendOps(); // also bumps the buffer-mutation epoch
++g_bufferContextGeneration;
// The generation moved AFTER the unregister bump above; re-open the
// memos again so no stamp can predate the generation change.
BumpBufferMutationEpoch();
InvalidateArrayBufferBindingCache();
InvalidateIndexedBufferBindingCache();
InvalidatePixelBufferBindingCaches();
// The rings' ids and persistent maps died with the context; drop the
// handles (no GL) and let the next draw / texture upload recreate them.
ResetRingForNewContext(UboRing());
ResetRingForNewContext(UnpackRing());
// R-11's server copies die with the context for the same reason the rings' ids do:
// the resource twins they are keyed by are about to be rebuilt against a new
// generation, and a shadow that outlived its twin would be looked up by a RECYCLED
// address on the next allocation - which is the quietest possible wrong answer.
MGL_SERVER_STAGED_DROP_ALL();
// tx's texture shadows die for the same reason, keyed by handle rather than address
// but with the same recycled-identity failure mode: a new context's allocator may
// hand out a {slot, gen} the old one's store still answers for.
MG_Record::ServerStagedTexture().DropAll();
// P11 B2: every retired T0 import's fence named work on the context that just ended,
// so all of them let go now. LIVE imports keep their reference: the next ensure
// imports them again on the new context (EnsureBufferResourceForHandle).
#if MOBILEGL_BUILD_DISAGGREGATED
ProcessDeferredT0Retires();
SweepT0Retired(/*contextGone=*/true);
#endif
}
void ProcessDeferredBufferReleases() {
// Runs on every draw; skip the context check, mutex and vector churn
// outright when nothing was enqueued since the last drain.
if (!g_hasDeferredBufferReleases.load(std::memory_order_acquire)) return;
if (!CanTouchGLNow()) return;
Vector<SharedPtr<BackendBufferResource>> releases;
{
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
releases.swap(g_deferredBufferReleases);
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
}
for (auto& resource : releases) {
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
if (glesResource->contextGeneration != g_bufferContextGeneration) {
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();
}
ScrubBufferBindingShadowsForId(glesResource->id);
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
glesResource->id = 0;
}
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: the T0 half of the deferred-release drain, run right after it at both sync
// points (ProcessDeferredBufferReleases itself moves verbatim, G5). Retired imports whose
// fence signalled let go of their AHardwareBuffer; a deferred T0 destroy is retired once
// the drain deleted its id (the fence then follows the delete) or its context is gone.
void ProcessDeferredT0Retires() {
if (!g_t0Retired.empty()) SweepT0Retired(false);
if (!g_hasT0DeferredDestroys.load(std::memory_order_acquire)) return;
Vector<SharedPtr<BackendBufferResource>> ready;
{
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
for (SizeT i = 0; i < g_t0DeferredDestroys.size();) {
auto* r = static_cast<GLESBufferResource*>(g_t0DeferredDestroys[i].get());
if (r->id == 0 || r->contextGeneration != g_bufferContextGeneration) {
ready.push_back(std::move(g_t0DeferredDestroys[i]));
g_t0DeferredDestroys[i] = std::move(g_t0DeferredDestroys.back());
g_t0DeferredDestroys.pop_back();
} else {
++i;
}
}
g_hasT0DeferredDestroys.store(!g_t0DeferredDestroys.empty(), std::memory_order_release);
}
for (auto& resource : ready)
RetireT0Import(*static_cast<GLESBufferResource*>(resource.get()));
}
#endif
// The same five questions, asked of the applier instead of the frontend object, with
// IDENTICAL semantics (D-A4):
// identity the slot table's twin at this handle, not GetBackendResource()
// size record.Desc.Width, not GetSize()
// freshness record.Serial vs syncedChangeSerial, not GetChangeSerial()
// map state record.HasLiveHostWrites AND the frontend's IsMapped() - see below
// the rest unchanged, and server-side to begin with
//
// HasLiveHostWrites is ALWAYS FALSE in P3a and is written by nobody; it is here so the
// phase that pushes persistent-mapped host writes can set it with no new record kind,
// and the assertion below is what stops that phase landing a silent semantic change.
//
// BECAUSE it is pinned false, it CANNOT stand in for the frontend's IsMapped() yet, and
// the probe still asks the object: an EMULATED (non-adopted) persistent map - under the
// 16 MiB adoption threshold, or with DisableLargeBufferAdoption, or with no
// EXT_buffer_storage - mutates the client's shadow with no call, no serial and no epoch,
// which is the entire reason the legacy probe asks a map question instead of a serial
// one. Answering only HasLiveHostWrites made such a buffer read draw-clean forever, so
// SyncPersistentMappedRange (D-N keeps it on the ensure path for all of P3a) was never
// reached again and the frame drew the last uploaded bytes with no diagnostic anywhere.
// The frontend read retires the moment P5 gives HasLiveHostWrites a producer, and it is
// the last frontend read in this function.
Bool IsBufferDrawCleanByHandle(MG_Pipe::MGPipeHandle res, const GLESBufferResource* resource,
const MG_State::GLState::BufferObject* frontend) {
if (!resource) return false;
const auto* twin = g_backendBufferResources.FindByHandle(res);
if (twin == nullptr || twin->get() != resource) return false;
if (resource->contextGeneration != g_bufferContextGeneration) return false;
if (resource->id == 0) return false;
if (resource->persistentMapped) {
return resource->persistentPtr != nullptr && resource->pendingResidentWrites.empty();
}
const auto* record = ResourceRecordOf(res);
if (record == nullptr) return false;
if (record->HasLiveHostWrites) return false;
// THE LAST FRONTEND READ IN THIS FUNCTION, AND P5 b1 RETIRES IT - under split
// only, because that is the only build where it is both wrong and replaceable.
//
// It asked the object whether it was mapped because HasLiveHostWrites was pinned
// false and an emulated persistent map mutates the shadow with no call, no serial
// and no epoch; answering from the record alone made such a buffer read
// draw-CLEAN forever, SyncPersistentMappedRange was never reached again, and the
// frame drew the last uploaded bytes with no diagnostic
// (MG_Test/SanityTest.cpp's DirectGLESBufferDrawProbe is exactly that case).
//
// TWO THINGS REPLACE IT AND BOTH HAD TO LAND FIRST: the record now carries the map
// state (the line above, from MGPSubData::HasLiveHostWrites), and the client
// pushes the mapped span by block at every validate point, so the serial moves for
// a write the application made with no API call. Under a spawn there is no object
// on this side to ask, which is why this was never going to stay a choice.
//
// A null object is the "no frontend to ask" case and is treated as "not mapped",
// which is what the record already says.
if (resource->pendingRespecify || !resource->storageInitialized) return false;
if (!resource->pendingRanges.empty()) return false;
if (resource->storageSize != static_cast<SizeT>(record->Desc.Width)) return false;
return resource->syncedChangeSerial.load(std::memory_order_acquire) == record->Serial;
}
// The handle arm of the ensure path. Same shape, same order, same branches; the four
// frontend reads become the applier's stored descriptor, its Serial and the shadow base
// the last content-carrying call handed over.
//
// It still takes the frontend object for ONE reason, recorded rather than hidden:
// BufferObject::SyncPersistentMappedRange() is one of the eleven Espryt sites
// ROADMAP/D-N explicitly keeps where it is for P3a (the per-site attribution table is
// P8's to execute). Every other line here is handle-shaped. When P8 moves that call to
// the client this signature loses its last frontend argument.
GLESBufferResource* EnsureBufferResourceForHandle(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, MG_Pipe::MGPipeHandle res) {
auto* resource = GetOrCreateBufferResourceForHandle(res);
if (!resource) return nullptr;
// The client's shadow base, RE-READ at every use, exactly as the legacy arm re-read
// MappedData() at every use. It is deliberately NOT resource->hostBytes here: two
// ordinary events move or free what a base recorded by an earlier call points at -
// a shadow resize (PipeResource::ResizeShadow is reserve + resize, so a grow past
// the reserve reallocates) and persistent-map adoption (the shadow is cleared and
// shrunk to fit) - and this path still holds the frontend object anyway, because
// D-N keeps SyncPersistentMappedRange here for all of P3a. hostBytes stays the
// fallback for the drains that have NO object (the readback flush), and it is
// nulled at both of those events.
const auto liveHostBase = [&resource, &bufferObject]() -> const Uint8* {
// M-2 / codex 3 / ID-52 item 3: under an ACTIVE TRANSPORT the server's staged
// copy (resource->hostBytes, filled by MGL_SERVER_STAGED_ADOPT in Ops_H_SubData /
// Ops_H_FlushRange) is the ONLY authoritative base. Preferring the frontend
// object's MappedData() here - which under inproc is always non-null, same process
// - meant every reduced-path drain read the CLIENT's shadow and NEVER the server's,
// so a corrupt staged upload rendered the frontend's correct bytes and the R-2.5
// 0xDD audit could not reach a draw. R-2 / table 3: honest inproc is inproc that
// does not read the client object's memory. Under monolith nothing changes.
return resource->hostBytes;
};
if (resource->contextGeneration != g_bufferContextGeneration) {
// The id (if any) belonged to a destroyed ES context.
resource->id = 0;
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
resource->contextGeneration = g_bufferContextGeneration;
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
resource->immutableStorage = false;
// m-5 / codex 5: OnBackendContextDestroyed ran MGL_SERVER_STAGED_DROP_ALL(), which
// frees every server shadow but does NOT null the hostBytes that name them - so a
// twin that SURVIVES a context loss still carries a base into the freed allocation.
// Null it here, where a surviving twin is re-armed - but ONLY when the shadow is
// really gone. This block also runs on a twin's FIRST ensure (contextGeneration
// starts mismatched), and there the shadow a preceding resource_subdata just staged
// is still live; nulling it then would drop the reduced path's own bytes (it did,
// and TriangleScenario read the wrong VBO). HasShadow is the discriminator: false
// after DropAll, true after an ordinary Adopt.
//
// TRANSPORT-GUARDED like the other two split hunks in this file (review v2 N-7):
// under MONOLITH transport in a disaggregated build the store never copies, Adopt
// never sets m_any, HasShadow answers false for everything, and this null would
// run on every twin's first ensure - harmless there only because liveHostBase()
// prefers MappedData() under monolith, and "under monolith nothing changes" should
// be true by construction rather than by luck.
if (ServerStaged().CopiesIntoServerStorage() && !ServerStaged().HasShadow(resource)) {
resource->hostBytes = nullptr;
}
#if defined(__ANDROID__) && MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: A T0 STORE OUTLIVES ITS CONTEXT. Its pages are the client's
// AHardwareBuffer, which this twin still holds, so the store is imported again on
// the new context - the client keeps writing through the same pointer and nothing
// has to be re-sent. A refusal here leaves the twin without a store and says so.
if (resource->externalAhb != nullptr) {
String why;
void* ptr = nullptr;
const Uint64 size = ResourceWidthOf(res);
const Uint id =
ResolveT0EntryPoints(why) ? ImportAhbAsBuffer(resource->externalAhb, size, &ptr, why) : 0;
if (id != 0) {
InstallT0Store(*resource, id, ptr, size);
MGLOG_I("Espryt T0: store {slot=%u, gen=%u} re-imported on the new context (%llu bytes)",
res.Slot, res.Gen, static_cast<unsigned long long>(size));
} else {
MGLOG_E("Espryt T0: store {slot=%u, gen=%u} could not be re-imported after a context loss - "
"%s; the store has no GPU backing",
res.Slot, res.Gen, why.c_str());
RetireT0Import(*resource);
}
}
#endif
}
// An immutable store nothing maps any more, retired here on the thread that can.
if (resource->immutableStorage && !resource->persistentMapped && resource->id != 0) {
NoteBufferIdDeleted(resource->id);
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
}
// Zero-copy coherent persistent buffer: nothing to (re)upload at draw time.
if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) {
DrainResidentWritesNow(*resource);
return resource;
}
if (resource->id == 0) {
const SizeT poolSize = ResourceWidthOf(res);
// The pool DECISION is the legacy one, unchanged: extent and residence only.
// An earlier cut added "and a shadow base was recorded", which silently stopped
// a buffer recycling pool ids - a change to D-F's pool behaviour, not to "the
// source of bytes" the conversion is allowed to move, and one that moves
// AcquireFromPool call counts StreamedArenaScenario observes.
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);
// M-3 / codex 4: this is a WHOLE-STORE upload from the base, and under split
// the base is the server shadow (M-2). For a store whose content the
// application SUPPLIED, zero-filled bytes past the staged coverage are not the
// application's - uploading them is the silent data loss the M-6 ruling
// forbids, and a gap is a missing record: Fatal by name. For a store the
// application ORPHANED the gap is its own undefined content and the upload is
// legal (ResourceContentIsDeclared, above). RequireCoverage is a no-op for the
// legacy arm's MappedData() and for a non-copying store.
if (ResourceContentIsDeclared(res)) {
MGL_SERVER_STAGED_REQUIRE(*resource, liveHostBase(), 0, poolSize, "pool_reuse_whole_store");
}
g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize, liveHostBase());
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(poolSize));
}
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
}
resource->syncedChangeSerial = ResourceSerialOf(res);
} else {
g_GLESFuncs.glGenBuffers(1, &resource->id);
if (resource->id == 0) {
MGLOG_E_ONCE("Failed to generate buffer object.");
MGLOG_E_ONCE("ES glGetError(): %s",
MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
return resource;
}
resource->storageInitialized = false;
resource->pendingRespecify = true;
}
}
// D-N keeps this call here for P3a. It can queue ranges and move the record, so
// everything below is read AFTER it.
// P5c (hd, CONTRACT-P5C §3.8): under an active transport the frontend accessor is a
// layer-1 surface ("buffer-legacy-arm") and the client's own pre-verb
// persistent-map push is the only producer; the call is skipped outright. Under
// monolith D-N's placement stands.
const auto* record = ResourceRecordOf(res);
const SizeT size = record != nullptr ? static_cast<SizeT>(record->Desc.Width) : 0;
if (size == 0) {
return resource;
}
const Uint64 serial = record->Serial;
const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(static_cast<BufferUsage>(record->Desc.Usage));
// Read AFTER SyncPersistentMappedRange above, and through liveHostBase for the
// reason written where it is declared.
const Uint8* const hostBase = liveHostBase();
// AND PUBLISH IT, because the readers that have NO frontend object read
// resource->hostBytes and nothing else: the readback flush (Ops_H_Readback), the
// kill-switch map arm of Ops_H_FlushRange, and the fp64 narrowing
// (SyncConvertedAttributeByHandle, whose legacy counterpart reads
// bufferObject->MappedData()). A twin is created LAZILY - D-A2's row makes
// Ops_H_Create a no-op - so the very first resource_respecify of a buffer finds
// FindBufferResourceForHandle == nullptr and its base is dropped on the floor; with
// glBufferData(..., data) followed by no further content call (the ordinary static
// vertex array) hostBytes then stayed null for the object's whole life and the fp64
// narrowing refused every draw, disabling the array. This is the one place that both
// holds the object and runs before every draw that uses the store, so it is where the
// base is refreshed. Only reached for a NON-adopted resource (the adopted arm
// returned above), so C-2's rule is intact: an adopted store's hostBytes stays null
// and its bytes are read through persistentPtr.
if (hostBase != nullptr) resource->hostBytes = hostBase;
// "DOES THE SHADOW THIS PATH IS ABOUT TO UPLOAD HOLD MEANINGFUL BYTES?" - and it has
// to be asked of the SAME thing the bytes come from. The legacy arm pairs
// bufferObject.MappedData() with bufferObject.HasDefinedContent() (RespecifyStorageNow)
// and this arm pairs the same two, so the source of the bytes and the statement about
// them can never disagree.
//
// P5c (hd, CONTRACT-P5C §3.6 / B1): with an active transport the answer is the
// DESCRIPTOR's bit - which the client publishes on ResourceCreate/Respecify - OR the
// staged coverage the content records behind it delivered. The coverage half is what
// keeps the answer equal to the frontend flag's in the one case the descriptor alone
// cannot see: an ORPHANING respecify (descriptor clear) followed by glBufferSubData,
// which defines content without a new descriptor - the bytes crossed as
// resource_subdata and the staged store's coverage is exactly "content defined since
// the last orphan". With no coverage and a clear bit the store is undefined by the
// application's own declaration and the upload stays a pure NULL reallocation.
const Bool shadowHasContent =
(record->Desc.HasDefinedContent != 0 ||
(ServerStaged().CopiesIntoServerStorage() ? ServerStaged().CoveredRunCount(resource) != 0
: resource->contentSinceRespecify));
const void* initialData = shadowHasContent ? hostBase : nullptr;
// M-3 / codex 4: a RespecifyStorageWith(..., initialData != nullptr) is a WHOLE-STORE
// [0, size) upload from the base, so it owes the same coverage the pending-range drain
// below owes - the two respecify arms were the readers M-6's "never widened" rule did
// not reach. No-op for the legacy arm's MappedData() and for a non-copying store; a
// Fatal{StageSnapshotTooNarrow, "respecify_whole_store"} under split when the
// DESCRIPTOR says the application supplied the content and the shadow's coverage does
// not span the store (a missing record), instead of uploading its zero-fill as content.
// NOT for a store the application orphaned: there `shadowHasContent` is the frontend
// object's flag, which the streaming idiom's partial glBufferSubData flips to true, and
// the bytes it did not write are undefined by its own glBufferData(NULL) - the rule at
// ResourceContentIsDeclared. Round 2 refused that idiom and aborted six joint entries.
const auto requireWholeStoreCoverage = [&]() {
if (initialData != nullptr && record->Desc.HasDefinedContent != 0) {
MGL_SERVER_STAGED_REQUIRE(*resource, static_cast<const Uint8*>(initialData), 0, size,
"respecify_whole_store");
}
};
if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) {
requireWholeStoreCoverage();
RespecifyStorageWith(*resource, size, usage, initialData, serial);
} else if (!resource->pendingRanges.empty()) {
// Same rule as Ops_H_Readback's, at the draw-time drain: with no frontend object
// `hostBase` is the server shadow and every queued range must be staged.
RenameSmallStoreForDrain(*resource, hostBase, size);
MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, hostBase, size, "ensure_flush_pending");
FlushPendingRangesFrom(*resource, hostBase, size);
resource->syncedChangeSerial = serial;
} else if (resource->syncedChangeSerial != serial) {
// Mutations this backend could not track (the table was unregistered between
// contexts); re-upload everything.
requireWholeStoreCoverage();
RespecifyStorageWith(*resource, size, usage, initialData, serial);
}
return resource;
}
void BindBufferId(GLenum target, Uint id) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (target == GL_ARRAY_BUFFER) {
if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) {
return;
}
g_boundArrayBufferId = id;
g_boundArrayBufferKnown = true;
}
g_GLESFuncs.glBindBuffer(target, id);
}
void InvalidateArrayBufferBindingCache() {
g_boundArrayBufferId = 0;
g_boundArrayBufferKnown = false;
}
void BindPixelPackBufferId(Uint id) {
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
return;
}
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, id);
g_boundPixelPackBufferId = id;
g_boundPixelPackBufferKnown = true;
}
void BindPixelUnpackBufferId(Uint id) {
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
return;
}
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id);
g_boundPixelUnpackBufferId = id;
g_boundPixelUnpackBufferKnown = true;
}
void InvalidatePixelBufferBindingCaches() {
g_boundPixelPackBufferId = 0;
g_boundPixelPackBufferKnown = false;
g_boundPixelUnpackBufferId = 0;
g_boundPixelUnpackBufferKnown = false;
}
void NoteBufferIdDeleted(Uint id) {
if (id == 0) {
return;
}
if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) {
InvalidateArrayBufferBindingCache();
}
ScrubBufferBindingShadowsForId(id);
}
namespace {
// Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range
// (same index + id + range) are skipped. isBase distinguishes a whole-buffer
// base bind from a sub-range bind. Fresh/reset context: every point is base(0)
// == unbound, which matches the GL default.
struct IndexedBufferBinding {
Uint id = 0;
GLintptr offset = 0;
GLsizeiptr size = 0;
Bool isBase = true;
// False when the driver's binding at this point is no longer described by the
// fields above and the next bind must be issued whatever it asks for. Set by
// InvalidateIndexedBufferBindingShadowsForId after a store was re-specified at
// a new size: the id is still bound, so the entry must NOT be scrubbed to
// base(0) (a later bind of 0 would then be false-skipped) - only distrusted.
Bool known = true;
};
constexpr SizeT kMaxIndexedBufferBindings = 64;
IndexedBufferBinding g_indexedUBOBindings[kMaxIndexedBufferBindings];
IndexedBufferBinding g_indexedSSBOBindings[kMaxIndexedBufferBindings];
// Transform feedback gets a shadow for a reason the other two do not have: the
// capture points are synced from the application's TOUCHED high-water mark, which
// deqp/glcts permanently raises to GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by
// clearing every point after each test case. Without a shadow every capture that
// uses fewer points than that (i.e. every INTERLEAVED_ATTRIBS capture) re-issued a
// redundant glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) for the unused
// tail immediately before glBeginTransformFeedback - calls a plain GL application
// never makes there, and the only thing MobileGL does differently from one.
//
// Unlike the UBO/SSBO points these are NOT context state: they belong to the bound
// transform feedback OBJECT, so XfbImpl::BindTransformFeedback drops the whole
// shadow to unknown on every object switch (InvalidateTransformFeedbackBindingShadows).
IndexedBufferBinding g_indexedXFBBindings[kMaxIndexedBufferBindings];
IndexedBufferBinding* IndexedBindingShadow(GLenum glTarget, Uint index) {
if (index >= kMaxIndexedBufferBindings) return nullptr; // out of range: never cache
if (glTarget == GL_UNIFORM_BUFFER) return &g_indexedUBOBindings[index];
if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index];
if (glTarget == GL_TRANSFORM_FEEDBACK_BUFFER) return &g_indexedXFBBindings[index];
return nullptr;
}
// glDeleteBuffers resets the deleted buffer's bindings (indexed and
// pixel pack/unpack ones included) to 0 in the current context; mirror
// that in the shadows, or a later buffer recycling the same name with a
// matching shadow entry would false-skip its rebind. Default
// IndexedBufferBinding{} == base(0) == the post-delete GL state.
void ScrubBufferBindingShadowsForId(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 = {};
}
for (auto& binding : g_indexedXFBBindings) {
if (binding.id == id) binding = {};
}
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
g_boundPixelPackBufferId = 0;
}
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
g_boundPixelUnpackBufferId = 0;
}
}
void InvalidateIndexedBufferBindingShadowsForId(Uint id) {
if (id == 0) return;
for (auto& binding : g_indexedUBOBindings) {
if (binding.id == id) binding.known = false;
}
for (auto& binding : g_indexedSSBOBindings) {
if (binding.id == id) binding.known = false;
}
for (auto& binding : g_indexedXFBBindings) {
if (binding.id == id) binding.known = false;
}
}
} // namespace
// The capture points belong to the bound transform feedback object, so a bind (or a
// delete, which reverts to the default object) replaces all of them at once with
// state this shadow has never seen. Distrust rather than scrub: the driver's bindings
// are whatever the newly bound object holds, which is NOT necessarily base(0), and
// scrubbing would let a later bind of 0 be false-skipped.
void InvalidateTransformFeedbackBindingShadows() {
for (auto& binding : g_indexedXFBBindings)
binding.known = false;
}
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) {
auto* s = IndexedBindingShadow(glTarget, index);
if (s && s->known && s->isBase && s->id == id) return;
g_GLESFuncs.glBindBufferBase(glTarget, index, id);
if (s) *s = {id, 0, 0, true, true};
}
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size) {
auto* s = IndexedBindingShadow(glTarget, index);
if (s && s->known && !s->isBase && s->id == id && s->offset == offset && s->size == size) return;
g_GLESFuncs.glBindBufferRange(glTarget, index, id, offset, size);
if (s) *s = {id, offset, size, false, true};
}
void InvalidateIndexedBufferBindingCache() {
for (auto& b : g_indexedUBOBindings)
b = {};
for (auto& b : g_indexedSSBOBindings)
b = {};
for (auto& b : g_indexedXFBBindings)
b = {};
}
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) {
ScrubBufferBindingShadowsForId(e.id);
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;
}
// --- Persistent-mapped bump rings (see Managers.h) ------------------------
// (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
// later uses stop retrying under this context.
Bool CreateRingStorage(PersistentRing& ring, SizeT minBytes) {
SizeT newSize = ring.initialBytes;
while (newSize < minBytes)
newSize *= 2;
if (newSize > ring.maxBytes) return false;
if (ring.store.id != 0) {
ring.retired.push_back(
{ring.store.id, ring.store.contextGeneration, DirectGLES::CurrentFrameSerial() + 1});
}
const Uint32 nextGeneration = ring.store.generation + 1;
ring.store.id = 0;
ring.store.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) {
// The dying id is what the array-buffer cache has recorded as
// bound; a later buffer recycling the name would false-skip.
InvalidateArrayBufferBindingCache();
g_GLESFuncs.glDeleteBuffers(1, &id);
id = 0;
} else {
ring.store.mappedPtr = static_cast<Uint8*>(ptr);
}
}
if (id == 0) {
MGLOG_E_ONCE("%s: persistent storage creation failed (%zu bytes); falling back to the "
"client-memory upload path.",
ring.label, newSize);
ring.store.creationFailed = true;
return false;
}
SizeT alignment = ring.fixedAlignment;
if (alignment == 0) {
const GLint capsAlignment = g_GLESCapabilities.UniformBufferOffsetAlignment;
alignment = capsAlignment > 0 ? static_cast<SizeT>(capsAlignment) : 256;
}
ring.store.id = id;
ring.store.size = newSize;
ring.store.head = 0;
ring.store.tail = 0;
ring.store.generation = nextGeneration;
ring.store.alignment = alignment;
ring.frameMarks.clear();
MGLOG_D("%s: %zu MiB persistent store ready (id %u, gen %u, align %zu).", ring.label,
newSize / (1024u * 1024u), id, nextGeneration, alignment);
return true;
}
namespace {
// Shared half of the availability gate (the per-ring kill switch sits in
// the exported wrappers). Reclamation rides the Present fence watermark;
// without working fences slots would never be provably GPU-idle (same rule
// as IsPoolable).
Bool RingAvailable(PersistentRing& ring) {
if (!g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers ||
!g_GLESFuncs.glFenceSync || !g_GLESFuncs.glGetSynciv) {
return false;
}
if (!CanTouchGLNow()) return false;
if (ring.store.contextGeneration != g_bufferContextGeneration) {
ResetRingForNewContext(ring);
}
return !ring.store.creationFailed;
}
// Division-based rounding fallback: the spec doesn't promise a power-of-two
// alignment. Slot offsets stay multiples of the alignment because every
// slot size is, and wrap padding restarts at ring offset 0.
inline SizeT RingAlignUp(SizeT size, SizeT alignment) {
if ((alignment & (alignment - 1)) == 0) {
return (size + alignment - 1) & ~(alignment - 1);
}
return (size + alignment - 1) / alignment * alignment;
}
} // namespace
// Bump-allocate `size` bytes out of `ring`.
//
// Fast path: a live store under the current context with room before both
// the wrap boundary and the in-flight tail. Touches no GL and probes no
// frame marks - callers sit behind the ring's availability gate, so the
// context checks have already run for this draw/upload. `tail` may be stale
// here (marks are only retired on Present and on the slow path); staleness
// is conservative - the in-flight span reads too large, the check fails,
// and the slow path retires marks and re-tries.
Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset) {
if (size == 0) return false;
auto& store = ring.store;
if (store.id != 0 && store.contextGeneration == g_bufferContextGeneration) {
const SizeT alignedSize = RingAlignUp(size, store.alignment);
// Ring sizes are the ring's initial size (a power of two) doubled
// some number of times, so the offset modulo reduces to a mask.
static_assert((kUboRingInitialBytes & (kUboRingInitialBytes - 1)) == 0,
"ring offset mask below requires power-of-two ring sizes");
static_assert((kUnpackRingInitialBytes & (kUnpackRingInitialBytes - 1)) == 0,
"ring offset mask below requires power-of-two ring sizes");
static_assert((kUploadRingInitialBytes & (kUploadRingInitialBytes - 1)) == 0,
"ring offset mask below requires power-of-two ring sizes");
const SizeT offset = static_cast<SizeT>(store.head & (store.size - 1));
if (offset + alignedSize <= store.size && store.head + alignedSize - store.tail <= store.size) {
store.head += alignedSize;
outOffset = offset;
return true;
}
}
return RingAllocateSlow(ring, size, outOffset);
}
Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset) {
if (!RingAvailable(ring)) return false;
auto& store = ring.store;
// Sizing the store first, so the request is rounded with the alignment
// the live store actually carries rather than the pre-creation default.
if (store.id == 0 && !CreateRingStorage(ring, RingAlignUp(size, store.alignment))) {
return false;
}
const SizeT alignedSize = RingAlignUp(size, store.alignment);
// Advance tail past every frame the GPU provably finished.
const Uint64 completed = DirectGLES::CompletedFrameSerial();
SizeT retiredMarks = 0;
for (const auto& mark : ring.frameMarks) {
if (mark.frameSerial > completed) break;
if (mark.headAtPresent > store.tail) store.tail = mark.headAtPresent;
++retiredMarks;
}
if (retiredMarks > 0) {
ring.frameMarks.erase(ring.frameMarks.begin(),
ring.frameMarks.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>(store.head % store.size);
if (offset + alignedSize > store.size) {
store.head += store.size - offset;
offset = 0;
}
if (store.head + alignedSize - store.tail > store.size) {
// In-flight span would overrun live slots: grow instead of overwrite.
if (CreateRingStorage(ring, std::max(store.size * 2, alignedSize))) {
offset = 0;
} else if (store.creationFailed) {
return false; // store lost; callers fall back to their legacy path
} else {
// At the size cap (>maxBytes in flight). First try to free room
// by waiting for the OLDEST in-flight frames to retire - a
// bounded wait that ends as soon as enough tail space exists,
// instead of draining the entire queue.
constexpr Uint64 kFrameWaitNs = 50ull * 1000 * 1000; // 50ms per frame
while (!ring.frameMarks.empty() && store.head + alignedSize - store.tail > store.size) {
const auto& oldest = ring.frameMarks.front();
if (!DirectGLES::WaitForFrameSerialCompleted(oldest.frameSerial, kFrameWaitNs)) {
break;
}
if (oldest.headAtPresent > store.tail) store.tail = oldest.headAtPresent;
ring.frameMarks.erase(ring.frameMarks.begin());
}
if (store.head + alignedSize - store.tail <= store.size) {
offset = static_cast<SizeT>(store.head % store.size);
if (offset + alignedSize > store.size) {
store.head += store.size - offset;
offset = 0;
}
store.head += alignedSize;
outOffset = offset;
return true;
}
// No usable fence covers the oldest frames: drain once rather than
// corrupt live slots.
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
store.tail = store.head;
ring.frameMarks.clear();
// Same-frame slots written before the drain may now be recycled by
// the very next allocations; a generation bump keeps later draws
// from rebinding those cached offsets.
++store.generation;
offset = static_cast<SizeT>(store.head % store.size);
if (offset + alignedSize > store.size) {
store.head += store.size - offset;
offset = 0;
}
}
}
store.head += alignedSize;
outOffset = offset;
return true;
}
namespace {
void DeleteOrphanRingBuffers() {
const std::lock_guard<std::mutex> lock(g_orphanRingBuffersMutex);
auto& orphans = OrphanRingBuffers();
if (orphans.empty()) return;
for (const OrphanRingBuffer& orphan : orphans) {
// A buffer of a share group that is gone was reclaimed with it.
if (orphan.contextGeneration != g_bufferContextGeneration) continue;
ScrubBufferBindingShadowsForId(orphan.id);
g_GLESFuncs.glDeleteBuffers(1, &orphan.id);
}
orphans.clear();
}
// Present()-time upkeep shared by both rings.
void RingOnPresent(PersistentRing& ring) {
if (!CanTouchGLNow()) return;
DeleteOrphanRingBuffers();
// Delete grown-away stores the GPU is provably done with.
const Uint64 completed = DirectGLES::CompletedFrameSerial();
for (SizeT i = ring.retired.size(); i-- > 0;) {
RetiredRingStore& entry = ring.retired[i];
const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration;
if (!staleContext && entry.retireSerial > completed) continue;
if (!staleContext && entry.id != 0) {
ScrubBufferBindingShadowsForId(entry.id);
g_GLESFuncs.glDeleteBuffers(1, &entry.id);
}
ring.retired[i] = ring.retired.back();
ring.retired.pop_back();
}
auto& store = ring.store;
if (store.id == 0 || store.contextGeneration != g_bufferContextGeneration) return;
// Retire completed marks here too — RingAllocate is the main consumer,
// but frames that used the ring for nothing would otherwise let the list
// grow one entry per Present, unboundedly.
SizeT retiredMarks = 0;
for (const auto& mark : ring.frameMarks) {
if (mark.frameSerial > completed) break;
if (mark.headAtPresent > store.tail) store.tail = mark.headAtPresent;
++retiredMarks;
}
if (retiredMarks > 0) {
ring.frameMarks.erase(ring.frameMarks.begin(),
ring.frameMarks.begin() + static_cast<std::ptrdiff_t>(retiredMarks));
}
// 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 (!ring.frameMarks.empty() && ring.frameMarks.back().frameSerial == serial) {
ring.frameMarks.back().headAtPresent = store.head;
} else {
ring.frameMarks.push_back({serial, store.head});
}
}
} // namespace
Bool UboRingAvailable() {
if (MG_Config::Features.EsprytDisableUboRing) return false;
return RingAvailable(UboRing());
}
Bool UboRingAllocate(SizeT size, SizeT& outOffset) {
return RingAllocate(UboRing(), size, outOffset);
}
UboRingSlotState UboRingPrepareSlot(UboRingAllocation& slot, Uint32 contentVersion, Uint64 frameSerial,
SizeT bindSize, void*& outWrite, Uint& outBufferId) {
if (MG_Config::Features.EsprytDisableUboRing) return UboRingSlotState::Unavailable;
PersistentRing& ring = UboRing();
if (!RingAvailable(ring)) return UboRingSlotState::Unavailable;
if (slot.ringGeneration == ring.store.generation && slot.frameSerial == frameSerial &&
slot.contentVersion == contentVersion) {
outBufferId = ring.store.id;
return UboRingSlotState::Reused;
}
SizeT offset = 0;
if (!RingAllocate(ring, bindSize, offset)) return UboRingSlotState::Failed;
// Read after the allocation: a grow mints a new store and generation.
outWrite = static_cast<Uint8*>(ring.store.mappedPtr) + offset;
slot = {contentVersion, ring.store.generation, frameSerial, offset};
outBufferId = ring.store.id;
return UboRingSlotState::Allocated;
}
void* UboRingMappedPtr() {
return UboRing().store.mappedPtr;
}
Uint UboRingBufferId() {
return UboRing().store.id;
}
Uint32 UboRingGeneration() {
return UboRing().store.generation;
}
void UboRingOnPresent() {
RingOnPresent(UboRing());
}
Bool UnpackRingAvailable() {
if (MG_Config::Features.EsprytDisableUnpackRing) return false;
return RingAvailable(UnpackRing());
}
Bool UnpackRingAllocate(SizeT size, SizeT& outOffset) {
// A request the ring could never satisfy even empty would otherwise walk
// the whole grow/drain ladder before failing.
if (size > kUnpackRingMaxBytes) return false;
return RingAllocate(UnpackRing(), size, outOffset);
}
void* UnpackRingMappedPtr() {
return UnpackRing().store.mappedPtr;
}
Uint UnpackRingBufferId() {
return UnpackRing().store.id;
}
SizeT UnpackRingMaxBytes() {
return kUnpackRingMaxBytes;
}
void UnpackRingOnPresent() {
RingOnPresent(UnpackRing());
}
void UploadRingOnPresent() {
RingOnPresent(UploadRing());
}
} // namespace BufferImpl
} // namespace MobileGL::MG_Backend::DirectGLES