mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (Espryt): adopt mesh-arena-sized stores into coherent persistent maps at definition, and stop the flush tiers from re-synchronizing them
This commit is contained in:
@@ -184,6 +184,15 @@ namespace MobileGL::MG_Config {
|
||||
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
||||
// mutable store ghost the whole destination on the CPU.
|
||||
Bool EsprytDisableInvalidateFlush = false;
|
||||
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
|
||||
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
|
||||
// persistently+coherently mapped storage at definition time (negative control /
|
||||
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
|
||||
// route for the same write stalls the thread or ghost-copies the whole arena on
|
||||
// this class of Mali driver, and the arena stops costing its size again in RAM.
|
||||
Bool DisableLargeBufferAdoption = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
|
||||
@@ -187,6 +187,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
||||
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
||||
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
|
||||
@@ -828,6 +828,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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:
|
||||
@@ -879,11 +885,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SizeT start = std::min(range.start, end);
|
||||
const SizeT size = end - start;
|
||||
if (size == 0) continue;
|
||||
if (mapUsable) {
|
||||
// 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,
|
||||
GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT);
|
||||
(GLsizeiptr)size, access);
|
||||
if (dst) {
|
||||
Memcpy(dst, bufferObject.MappedData() + start, size);
|
||||
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
|
||||
@@ -1055,17 +1077,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->pendingRanges.Add({offset, offset + size});
|
||||
return;
|
||||
}
|
||||
// An adopted zero-copy persistent store already HAS the bytes (the
|
||||
// frontend wrote them through the coherent mapping); a driver upload
|
||||
// here would be a self-copy that re-synchronizes what coherent mapping
|
||||
// made free.
|
||||
if (resource->persistentMapped && resource->persistentPtr) {
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
return;
|
||||
}
|
||||
// An immediate glBufferSubData resolves the WAR hazard against frames
|
||||
// still referencing this store on the CPU on some drivers - Mali parks
|
||||
// the thread in osup_sync_object_wait until every referencing job
|
||||
// retires, which serialized Minecraft 26.3's per-frame UBO/chunk-mesh
|
||||
// update streams into ~1 fps. Queue the range instead (the shadow
|
||||
// already holds the bytes) and let draw-time sync push the merged
|
||||
// ranges through the staging ring. The zero-copy persistent store
|
||||
// keeps the legacy immediate upload: draw-time sync never flushes
|
||||
// ranges for it, and its mapping publishes writes by itself.
|
||||
if ((resource->persistentMapped && resource->persistentPtr) ||
|
||||
MG_Config::Features.EsprytDisableUploadRing) {
|
||||
// ranges through the staging ring.
|
||||
if (MG_Config::Features.EsprytDisableUploadRing) {
|
||||
UploadRangeNow(*resource, bufferObject, offset, offset + size);
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
return;
|
||||
@@ -1087,13 +1114,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
// Same WAR-hazard rule as Ops_SubData: an immediate upload (mapped or
|
||||
// glBufferSubData) can park the thread on Mali until the frames still
|
||||
// referencing this store retire. Queue the range for the staged-copy
|
||||
// flush at draw-time sync; only the zero-copy persistent store and the
|
||||
// negative-control kill switch keep the immediate paths below.
|
||||
if (!(resource->persistentMapped && resource->persistentPtr) &&
|
||||
!MG_Config::Features.EsprytDisableUploadRing) {
|
||||
// An adopted zero-copy persistent store already HAS the bytes: the
|
||||
// frontend shadow IS the coherent mapping the app (or UploadSubData)
|
||||
// wrote into, so publishing is free. The self-copy that used to run
|
||||
// here mapped a buffer this backend keeps persistently mapped (an
|
||||
// INVALID_OPERATION whose fallback was a WAR-stalling
|
||||
// glBufferSubData).
|
||||
if (resource->persistentMapped && resource->persistentPtr) {
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
return;
|
||||
}
|
||||
|
||||
// Same WAR-hazard rule as Ops_SubData: an immediate synchronized upload
|
||||
// (mapped or glBufferSubData) can park the thread on Mali until the
|
||||
// frames still referencing this store retire. Queue the range for the
|
||||
// staged flush at draw-time sync; the negative-control kill switch
|
||||
// keeps the immediate paths below.
|
||||
if (!MG_Config::Features.EsprytDisableUploadRing) {
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.Add(range);
|
||||
return;
|
||||
@@ -2628,7 +2665,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
||||
// TEMP-EXP (leak texture deletes): /sdcard/MG/exp_leak_texture_deletes.
|
||||
// Discriminator for the mali-mem-purge hiccup theory: never hand the
|
||||
// driver a texture free, so the purge daemon has nothing to reclaim.
|
||||
static const Bool s_expLeakTextureDeletes = [] {
|
||||
FILE* f = std::fopen("/sdcard/MG/exp_leak_texture_deletes", "rb");
|
||||
if (!f) return false;
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}();
|
||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures &&
|
||||
!s_expLeakTextureDeletes) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||
if (m_bufferImageSplitViewId != 0) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "BufferObject.h"
|
||||
|
||||
#include <Config.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
@@ -126,6 +128,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// distinguishes the two cases, and it is cleared just above.
|
||||
m_storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT;
|
||||
NotifyRespecify();
|
||||
TryAdoptLargeStorage();
|
||||
}
|
||||
|
||||
void BufferObject::Resize(SizeT size) {
|
||||
@@ -144,6 +147,33 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_isImmutableStorage = true;
|
||||
m_storageFlags = storageFlags;
|
||||
NotifyRespecify();
|
||||
TryAdoptLargeStorage();
|
||||
}
|
||||
|
||||
// Back a LARGE store with the backend's persistently+coherently mapped GPU
|
||||
// storage the moment it is (re)defined, without waiting for the app to map it.
|
||||
// Minecraft 26.3 streams chunk meshes into 128MB vertex arenas with plain
|
||||
// glNamedBufferSubData - the one write API that carries no synchronization
|
||||
// hint - and on Mali every route that hands the driver a write into a busy
|
||||
// MUTABLE store either parks the calling thread (glBufferSubData, and
|
||||
// glMapBufferRange even with GL_MAP_UNSYNCHRONIZED_BIT) or ghost-copies the
|
||||
// whole destination on a driver worker (staged glCopyBufferSubData, and a
|
||||
// range-invalidating map: ~167ms per touched arena, the recurring in-world
|
||||
// hiccup). An adopted coherent map is the one shape with NO per-write driver
|
||||
// call at all: every SubData lands as a plain memcpy into GPU-visible memory,
|
||||
// and the shadow copy is dropped (a 128MB arena stops costing 128MB of RAM).
|
||||
// Only attempted for stores the size of mesh arenas: small buffers keep the
|
||||
// shadow model whose draw-time flush already prices them correctly.
|
||||
void BufferObject::TryAdoptLargeStorage() {
|
||||
constexpr SizeT kLargeBufferAdoptBytes = 16u * 1024u * 1024u;
|
||||
if (MG_Config::Features.DisableLargeBufferAdoption) return;
|
||||
if (m_size < kLargeBufferAdoptBytes) return;
|
||||
if (m_resource.IsGpuResident()) return;
|
||||
if (m_isMapped) return;
|
||||
if (g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) return;
|
||||
if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) {
|
||||
m_resource.AdoptPersistentMap(base);
|
||||
}
|
||||
}
|
||||
|
||||
void BufferObject::UploadData(DataPtr data, SizeT atOffset) {
|
||||
|
||||
@@ -211,6 +211,10 @@ namespace MobileGL {
|
||||
// Sizes the store for a (re)definition, renewing an adopted GPU-resident
|
||||
// mapping across it. See the definition for why the renewal is not optional.
|
||||
void RedefineStorage(SizeT size);
|
||||
// Backend-initiated coherent adoption for mesh-arena-sized stores; see the
|
||||
// definition for the driver behavior that makes every other write route to
|
||||
// a busy large mutable store a frame-scale stall.
|
||||
void TryAdoptLargeStorage();
|
||||
void NotifyRespecify();
|
||||
void NotifySubData(SizeT offset, SizeT size);
|
||||
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
|
||||
|
||||
Reference in New Issue
Block a user