diff --git a/MobileGL/Config.h b/MobileGL/Config.h index a51d7e58..38f61d2c 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -176,6 +176,14 @@ namespace MobileGL::MG_Config { // upload ring (negative control / driver-bug escape hatch; the immediate // upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali). Bool DisableUploadRing = false; + // MOBILEGL_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE | + // INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight + // to the upload ring's staged glCopyBufferSubData (negative control / escape + // hatch for a driver whose range-invalidating map misbehaves). The map tier is + // what keeps a partial write into a large in-flight buffer priced by the RANGE: + // on Mali both the immediate glBufferSubData and a staged copy into a busy + // mutable store ghost the whole destination on the CPU. + Bool DisableInvalidateFlush = 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, diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index e2d0f579..0c3b6cc2 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -186,6 +186,7 @@ namespace MobileGL::MG_ConfigLoader { features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); features.DisableUnpackRing = QueryEnvFlag("MOBILEGL_DISABLE_UNPACK_RING"); features.DisableUploadRing = QueryEnvFlag("MOBILEGL_DISABLE_UPLOAD_RING"); + features.DisableInvalidateFlush = QueryEnvFlag("MOBILEGL_DISABLE_INVALIDATE_FLUSH"); features.EsprytForceDepthStencilReadbackEmulation = QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION"); features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS"); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 6324359b..ac5f4af5 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -829,10 +829,32 @@ namespace MobileGL::MG_Backend::DirectGLES { } // Push every queued range of `resource` from the shadow into the backend - // store. Ranges go through the staging ring + glCopyBufferSubData when the - // ring is usable - the copy resolves the WAR hazard against in-flight - // frames on the GPU timeline, where it is mere job ordering - and fall - // back to the direct (potentially stalling) glBufferSubData otherwise. + // 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_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. void FlushPendingRangesNow(GLESBufferResource& resource, BufferObject& bufferObject) { #ifdef TRACY_ENABLE @@ -848,19 +870,26 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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). - // - // The ranges are copied AS QUEUED (VecRange1D::Add already merges - // near-adjacent ones): this driver runs buffer copies as worker-thread - // memcpys, so BYTES are the cost axis - collapsing a scattered flush - // into its union re-copied nearly whole chunk-mesh arenas every frame - // and saturated the copy worker during camera pans. const SizeT limit = std::min(bufferObject.GetSize(), resource.storageSize); + const Bool mapUsable = !MG_Config::Features.DisableInvalidateFlush && + 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 (mapUsable) { + BindBufferId(TempBufferTarget, resource.id); + void* dst = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start, + (GLsizeiptr)size, + GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT); + if (dst) { + Memcpy(dst, bufferObject.MappedData() + start, size); + g_GLESFuncs.glUnmapBuffer(TempBufferTarget); + continue; + } + } SizeT ringOffset = 0; if (ringUsable && size <= kUploadRingMaxBytes && RingAllocate(g_uploadRing, size, ringOffset)) {