mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Refactor] (Espryt): take the buffer ops by handle and payload instead of by frontend object reference
This commit is contained in:
@@ -8,6 +8,14 @@
|
||||
|
||||
#include "Managers.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P3a: the handle-shaped resource op table, the applier's records and the reverse channel.
|
||||
// Both headers are compiled into the library only under push, so they are included here
|
||||
// under the same condition - a pull build must gain no declaration it cannot link.
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
#include "Utils.h"
|
||||
#include "DirectGLES.h"
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
@@ -883,6 +891,293 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return DirectGLES::IsBackendContextCurrentOnThisThread();
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// 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());
|
||||
}
|
||||
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
|
||||
// Ring machinery shared with the UBO/unpack rings; defined further down in
|
||||
// this same unnamed namespace.
|
||||
Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset);
|
||||
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(g_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;
|
||||
|
||||
// Push every queued range of `resource` from the shadow into the backend
|
||||
// store, without ever letting a driver resolve the WAR hazard against
|
||||
// in-flight frames at the WHOLE BUFFER's expense. Three tiers:
|
||||
//
|
||||
// 1. glMapBufferRange(WRITE | INVALIDATE_RANGE) + memcpy. The entire
|
||||
// mapped range is rewritten from the authoritative shadow, so
|
||||
// declaring its old bytes dead is exact - and it lets the driver
|
||||
// swap fresh pages in for JUST that range. This is the only tier
|
||||
// whose cost scales with the RANGE on this Mali driver: both the
|
||||
// immediate glBufferSubData (pre-queueing) and a staged
|
||||
// glCopyBufferSubData into a busy MUTABLE store ghost the whole
|
||||
// destination with a worker-thread memcpy - Minecraft 26.3 streams
|
||||
// ~1MB section meshes into 128MB arenas about nine times a frame
|
||||
// during a camera pan, and 9 x 128MB of ghosting per frame is
|
||||
// ~380ms, the measured 2-4 fps. (Backing the arenas with immutable
|
||||
// stores also kills the ghost, but eagerly commits every arena's
|
||||
// full extent - +hundreds of MB - which LMK'd the whole device.)
|
||||
// 2. The staging ring + glCopyBufferSubData: the copy is ordered on
|
||||
// the GPU timeline, no CPU wait (MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH
|
||||
// forces this tier as the map path's negative control).
|
||||
// 3. Direct glBufferSubData (potentially stalling) when neither the
|
||||
// map entry points nor the ring exist.
|
||||
//
|
||||
// The ranges are flushed AS QUEUED (VecRange1D::Add already merges
|
||||
// near-adjacent ones): bytes, not flush calls, are the cost axis here,
|
||||
// and collapsing a scattered flush into its union re-copied nearly whole
|
||||
// chunk-mesh arenas every frame.
|
||||
// The caller owns syncedChangeSerial; this only drains the queue.
|
||||
//
|
||||
// ONE body for both arms, and deliberately so: the three-tier decision (whole-buffer
|
||||
// orphan map / >= 128 KiB range-invalidating map / staged ring copy) is what the MC
|
||||
// 26.3 p99 depends on, and a second copy of it for the handle arm is exactly how a
|
||||
// tier silently changes. The arms differ only in where `hostBase` and `frontendSize`
|
||||
// come from.
|
||||
void FlushPendingRangesFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize) {
|
||||
#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.
|
||||
const Bool wholeBuffer = start == 0 && end == limit && limit == resource.storageSize;
|
||||
if (mapUsable && (wholeBuffer || size >= kInvalidateRangeMinBytes)) {
|
||||
BindBufferId(TempBufferTarget, resource.id);
|
||||
const GLbitfield access =
|
||||
GL_MAP_WRITE_BIT |
|
||||
(wholeBuffer ? GL_MAP_INVALIDATE_BUFFER_BIT : GL_MAP_INVALIDATE_RANGE_BIT);
|
||||
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(g_uploadRing, size, ringOffset)) {
|
||||
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, hostBase + start, size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, g_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);
|
||||
} else {
|
||||
UploadRangeFrom(resource, hostBase, start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FlushPendingRangesNow(GLESBufferResource& resource, BufferObject& bufferObject) {
|
||||
FlushPendingRangesFrom(resource, bufferObject.MappedData(), bufferObject.GetSize());
|
||||
}
|
||||
|
||||
// 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(g_uploadRing, size, ringOffset)) {
|
||||
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, g_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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The two-argument spelling the legacy arm's call sites use, kept so those sites
|
||||
// are the SAME text in both builds (G1). The object was never read.
|
||||
void DrainResidentWritesNow(GLESBufferResource& resource, BufferObject& bufferObject) {
|
||||
(void)bufferObject;
|
||||
DrainResidentWritesNow(resource);
|
||||
}
|
||||
#else
|
||||
// (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.
|
||||
@@ -1118,6 +1413,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
// EXT_buffer_storage bit values (same numeric values as the desktop ARB
|
||||
// tokens); defined locally so this compiles regardless of which GLES headers
|
||||
@@ -1482,6 +1778,399 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BumpBufferMutationEpoch();
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P3a: the same seven ops plus the create/unmap pair, BY HANDLE ------------
|
||||
//
|
||||
// Every body below is its Ops_* counterpart above with exactly the substitutions
|
||||
// D-A2's table names and nothing else: the frontend object's GetSize() / GetUsage()
|
||||
// / HasDefinedContent() / MappedData() / GetChangeSerial() become the applier
|
||||
// record's Desc.Width / Desc.Usage / Desc.HasDefinedContent, the shadow base the
|
||||
// call carried, and the record's server-owned Serial. Every branch survives -
|
||||
// pendingRespecify early-out, the off-thread queue, the adopted zero-copy stamp,
|
||||
// the upload-ring kill switch, the Mali WAR-stall queue-only default.
|
||||
//
|
||||
// NOTHING here reads a frontend type. The one thing a handle op cannot do is ask
|
||||
// an object for its shadow, which is why GLESBufferResource::hostBytes exists: the
|
||||
// three content-carrying calls hand the base over and the later drains read it.
|
||||
|
||||
// The applier's record for this resource, or null when the client never created it
|
||||
// (or created it into a slot that has since been recycled).
|
||||
const MG_Pipe::MGPipeResourceRecord* ResourceRecordOf(MG_Pipe::MGPipeHandle res) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr;
|
||||
const auto& records = MG_Pipe::MGPipeApplier().Resources;
|
||||
if (res.Slot >= records.size()) return nullptr;
|
||||
const auto& record = records[res.Slot];
|
||||
if (!record.Live || record.Gen != res.Gen) return nullptr;
|
||||
return &record;
|
||||
}
|
||||
|
||||
// The server-owned MGGen this backend mirrors in syncedChangeSerial. Zero for a
|
||||
// resource with no record, which is the same "never synced" answer a frontend
|
||||
// change serial of zero gave.
|
||||
Uint64 ResourceSerialOf(MG_Pipe::MGPipeHandle res) {
|
||||
const auto* record = ResourceRecordOf(res);
|
||||
return record != nullptr ? record->Serial : 0;
|
||||
}
|
||||
SizeT ResourceWidthOf(MG_Pipe::MGPipeHandle res) {
|
||||
const auto* record = ResourceRecordOf(res);
|
||||
return record != nullptr ? static_cast<SizeT>(record->Desc.Width) : 0;
|
||||
}
|
||||
|
||||
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);
|
||||
if (initialBytes != nullptr && resource != nullptr) {
|
||||
// A respecify's companion pointer IS the shadow base (offset 0).
|
||||
resource->hostBytes = static_cast<const Uint8*>(initialBytes);
|
||||
}
|
||||
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);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||
resource->id = 0;
|
||||
resource->immutableStorage = false;
|
||||
}
|
||||
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);
|
||||
if (!resource) return;
|
||||
if (bytes != nullptr) resource->hostBytes = static_cast<const Uint8*>(bytes) - offset;
|
||||
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);
|
||||
if (bytes != nullptr) resource->hostBytes = static_cast<const Uint8*>(bytes) - start;
|
||||
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) {
|
||||
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) {
|
||||
auto* resource = FindBufferResourceForHandle(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;
|
||||
const SizeT size = std::min<SizeT>(static_cast<SizeT>(record.Size), resource->storageSize);
|
||||
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.
|
||||
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.
|
||||
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);
|
||||
}
|
||||
|
||||
void Ops_H_Destroy(MG_Pipe::MGPipeHandle res) {
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
const BufferBackendOps g_glesBufferBackendOps = {
|
||||
.Respecify = Ops_RespecifyTracked,
|
||||
.SubData = Ops_SubDataTracked,
|
||||
@@ -1549,6 +2238,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void RegisterBufferBackendOps() {
|
||||
MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P3a: the handle-shaped table goes up beside it, at the same two bring-up sites
|
||||
// and with the same lifetime. Registration is UNCONDITIONAL, exactly as
|
||||
// BufferBackendOps' is: 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);
|
||||
#endif
|
||||
// Frontend writes issued while ops were unregistered advanced change
|
||||
// serials with no per-op bump; re-open every draw-clean memo.
|
||||
BumpBufferMutationEpoch();
|
||||
@@ -1558,6 +2256,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (MG_State::GLState::GetBufferBackendOps() == &g_glesBufferBackendOps) {
|
||||
MG_State::GLState::SetBufferBackendOps(nullptr);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::MGPipeGetResourceOps() == &g_glesResourceOps) {
|
||||
MG_Pipe::MGPipeSetResourceOps(nullptr);
|
||||
}
|
||||
#endif
|
||||
// From here on frontend writes bypass the tracked ops entirely.
|
||||
BumpBufferMutationEpoch();
|
||||
InvalidateArrayBufferBindingCache();
|
||||
@@ -1621,7 +2324,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return static_cast<GLESBufferResource*>(bufferObject->GetBackendResource().get());
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// 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, not IsMapped()
|
||||
// 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.
|
||||
Bool IsBufferDrawCleanByHandle(MG_Pipe::MGPipeHandle res, const GLESBufferResource* resource) {
|
||||
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 MOBILEGL_PIPE_VERIFY
|
||||
MOBILEGL_ASSERT(!record->HasLiveHostWrites,
|
||||
"MGPipeResourceRecord::HasLiveHostWrites is set, but P3a has no producer for it");
|
||||
#endif
|
||||
if (record->HasLiveHostWrites) return false;
|
||||
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;
|
||||
}
|
||||
#endif
|
||||
|
||||
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (ResourceSubsystemEnabled()) {
|
||||
return IsBufferDrawCleanByHandle(HandleOfBuffer(frontend), resource);
|
||||
}
|
||||
#endif
|
||||
// Identity first: a respecify path can hand the frontend a NEW resource; the
|
||||
// memoed pointer is then stale (and only kept alive by the caller's shadow).
|
||||
if (!resource || resource != frontend->GetBackendResource().get()) return false;
|
||||
@@ -1644,12 +2387,142 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return resource->syncedChangeSerial.load(std::memory_order_acquire) == frontend->GetChangeSerial();
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// 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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 reseed is a whole-buffer upload and its only source of bytes on this
|
||||
// arm is the shadow base the client last sent; with none, a recycled id could
|
||||
// not be reseeded and a fresh store is the honest answer.
|
||||
const Uint reused = (poolSize > 0 && !resource->persistentMapped &&
|
||||
resource->hostBytes != nullptr)
|
||||
? AcquireFromPool(poolSize)
|
||||
: 0;
|
||||
if (reused != 0) {
|
||||
resource->id = reused;
|
||||
resource->storageSize = poolSize;
|
||||
resource->storageInitialized = true;
|
||||
resource->pendingRespecify = false;
|
||||
BindBufferId(TempBufferTarget, reused);
|
||||
g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize, resource->hostBytes);
|
||||
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.
|
||||
if (bufferObject) bufferObject->SyncPersistentMappedRange();
|
||||
|
||||
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));
|
||||
const void* initialData = record->Desc.HasDefinedContent != 0 ? resource->hostBytes : nullptr;
|
||||
|
||||
if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) {
|
||||
RespecifyStorageWith(*resource, size, usage, initialData, serial);
|
||||
} else if (!resource->pendingRanges.empty()) {
|
||||
FlushPendingRangesFrom(*resource, resource->hostBytes, size);
|
||||
resource->syncedChangeSerial = serial;
|
||||
} else if (resource->syncedChangeSerial != serial) {
|
||||
// Mutations this backend could not track (the table was unregistered between
|
||||
// contexts); re-upload everything.
|
||||
RespecifyStorageWith(*resource, size, usage, initialData, serial);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
#endif
|
||||
|
||||
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (!bufferObject) return nullptr;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (ResourceSubsystemEnabled()) {
|
||||
const MG_Pipe::MGPipeHandle res = HandleOfBuffer(bufferObject.get());
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) {
|
||||
// The client never created this resource on the wire. There is deliberately
|
||||
// no fall-back to the legacy arm: silently twinning it off the frontend
|
||||
// object would hide a missing emission behind a working picture, which is
|
||||
// exactly what the subsystem A/B exists to make visible.
|
||||
MGLOG_E_ONCE("MGPipe: buffer %u has no resource handle - the resource family is switched "
|
||||
"over but nothing emitted resource_create for it",
|
||||
bufferObject->GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
return EnsureBufferResourceForHandle(bufferObject, res);
|
||||
}
|
||||
#endif
|
||||
|
||||
auto* resource = static_cast<GLESBufferResource*>(bufferObject->GetBackendResource().get());
|
||||
if (!resource) {
|
||||
auto newResource = MakeShared<GLESBufferResource>();
|
||||
|
||||
@@ -712,6 +712,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// neither the object nor its lifetime id exists on this side and every one of them
|
||||
// has to receive the handle in a payload instead.
|
||||
MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject);
|
||||
|
||||
// The handle arms of the two draw-path entry points below. IsBufferDrawCleanByHandle
|
||||
// asks the applier the same five questions IsBufferDrawClean asks the frontend object,
|
||||
// with identical semantics (D-A4); EnsureBufferResourceForHandle is the ensure path
|
||||
// driven by the applier's descriptor and the shadow base the call carried.
|
||||
Bool IsBufferDrawCleanByHandle(MG_Pipe::MGPipeHandle res, const GLESBufferResource* resource);
|
||||
GLESBufferResource* EnsureBufferResourceForHandle(
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, MG_Pipe::MGPipeHandle res);
|
||||
#endif
|
||||
|
||||
// Registered as the frontend's BufferBackendOps at backend init and on
|
||||
|
||||
Reference in New Issue
Block a user