From 31e6a7d6d8f9a3e1d45dbe844c8942f59c1d4290 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 8 Sep 2026 12:51:41 -0400 Subject: [PATCH] [Refactor] (Espryt): resolve the current framebuffers from the pushed records and keep the shadowed default bind and the missing fast path exactly as they are --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 365 +++++++++++++++++- 1 file changed, 347 insertions(+), 18 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 12fc08f1..7302ecc4 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -192,6 +192,67 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif } +#if MOBILEGL_PIPE_PUSH + // --------------------------------------------------------------------------------- + // P4a: the four family arm predicates the draw path below gates on. + // --------------------------------------------------------------------------------- + // + // TEMPORARY AND SAID SO. D-K3 gives the phase four ResolveSubsystemArm() latches, + // beside ResolveResourceSubsystemArm / ResolveVertexInputSubsystemArm in Managers.cpp - + // which is package D's file for the whole phase, so the draw-path package cannot write + // them. It is landed BEFORE D, so it carries its own bit tests until those latches exist; + // at the rebase every one of these becomes a call to D's, which additionally + // + // * LOGS each dependency refusal with one MGLOG_E naming BOTH bits, and + // * reaches PipeSubsystemArmVerdict::NoArm and STOPS with the named + // Fatal{PipeLegacyMemosDisabled, ...} when MOBILEGL_PIPE_LEGACY_MEMOS is 0 - which none + // of these four families' legacy arms survives (D-K3's table: the four g_fboSynced* + // arrays, the twin's cheap-gate trio, UnitSamplerLookupMemo's WeakPtr rows and + // g_programTwinLookupMemo are all pre-handle arms). + // + // The dependency DIRECTIONS are copied here verbatim, so a mask that half-runs is refused + // in the meantime too and the A/B is not silently wrong; the refusal log and the stop are + // D's and are deliberately not duplicated here, because two writers of one diagnostic is + // how the two drift. + // + // Latched once per process for the reason Managers.h gives for the other two: the two arms + // keep their state in different places, so an answer that changed mid-run would strand + // everything already built against the previous one. + static Bool EsprytDrawTextureResourceHandlesEnabled() { + // Bit 10 requires bit 7: a buffer texture's BufferForTexBuffer names a Buffer handle + // and only bit 7 puts twins in that table (D-K2). + static const Bool enabled = + (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemTextureResources) != 0 && + (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemResources) != 0; + return enabled; + } + + static Bool EsprytDrawFramebufferHandlesEnabled() { + // Bit 9 requires bit 10: every MGPSurface::Res names a Texture or Renderbuffer handle. + static const Bool enabled = + (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemFramebuffer) != 0 && + EsprytDrawTextureResourceHandlesEnabled(); + return enabled; + } + + static Bool EsprytDrawSamplerHandlesEnabled() { + // Bit 11 requires bit 10: every MGPBoundView::Texture and MGPImageView::Res names a + // Texture handle, and without bit 10 every lookup would miss and the walk would + // `continue` past a unit it should have unbound. + static const Bool enabled = + (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemSamplers) != 0 && + EsprytDrawTextureResourceHandlesEnabled(); + return enabled; + } + + static Bool EsprytDrawProgramHandlesEnabled() { + // Bit 12 depends on nothing: a ShaderCso handle names no texture and no buffer. + static const Bool enabled = + (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemPrograms) != 0; + return enabled; + } +#endif // MOBILEGL_PIPE_PUSH + static Bool IsDualSourceBlendFactor(BlendFactor v) { switch (v) { case BlendFactor::Src1Color: @@ -2226,6 +2287,147 @@ namespace MobileGL::MG_Backend::DirectGLES { g_fboSyncedBackendIdGenerations[SizeT(target)] = g_attachmentBackendIdGeneration; } +#if MOBILEGL_PIPE_PUSH + // P4a e1 (D-C2, D-B3). The framebuffer family's handle arm. + // + // THE FOUR-PART MEMO BECOMES TWO VALUES, and the halves are not symmetric. + // g_fboSyncedSlotVersions / ObjectVersions / Objects are three answers to one question + // - "has the state the client would describe moved?" - asked of the frontend from the + // backend. The applier answers it with one number: FramebufferSerial is bumped by the + // applier before the backend is told anything, on every set_framebuffer_state it + // accepts, and a record the client's ContentHash suppressor withheld provably describes + // state that did not move. So one Uint64 compare replaces the three. + // + // The FOURTH stays, and must: g_attachmentBackendIdGeneration answers "did *I* re-mint + // a driver texture id", which no client-side version can answer, and dropping it is the + // shape of bug commit d7655247 fixed on the buffer side (D-B3, D-O). The ES context + // generation joins it for the same reason the legacy arm's other memos carry it - the + // driver FBO names die with the context. + // Kept PER TARGET, like the arrays it replaces, and for the one reason that survives + // the collapse: ForceBindCurrentFBO syncs a single target and has to be able to say so. + // What collapses is the three-value "has the state moved" question into one serial. + struct SyncedFramebufferSerialMemo { + Uint64 serial = 0; // the FramebufferSerial this target was last synced at + Uint64 backendIdGeneration = 0; + Uint contextGeneration = 0; + Bool valid = false; + }; + static Array + g_fboSyncedSerials{}; + + static void InvalidateFramebufferHandleArmMemos() { + for (auto& memo : g_fboSyncedSerials) memo.valid = false; + } + + // Record that `target` now reflects the applier's record as of `serial`. The two + // generations ride along for the reason D-B3 keeps them: they answer questions about + // the DRIVER's own ids that no client-side version can answer. + static void StampSyncedFramebufferSerial(FramebufferTarget target, Uint64 serial) { + auto& memo = g_fboSyncedSerials[SizeT(target)]; + memo.serial = serial; + memo.backendIdGeneration = g_attachmentBackendIdGeneration; + memo.contextGeneration = g_backendContextGeneration; + memo.valid = true; + } + + static Bool SyncedFramebufferSerialIsCurrent(FramebufferTarget target, Uint64 serial) { + const auto& memo = g_fboSyncedSerials[SizeT(target)]; + return memo.valid && memo.serial == serial && + memo.backendIdGeneration == g_attachmentBackendIdGeneration && + memo.contextGeneration == g_backendContextGeneration; + } + + // The handle arm of SyncCurrentFBO. Returns false when it declined - which is not a + // failure and is the only honest answer while no client has emitted yet. + // + // NO LIVE RECORD IS A MISS, NEVER A HIT, and it is the same rule the vertex-input arm + // states at SyncVaoAttributeBuffersByHandle. What is NOT the test, and this is a trap + // worth naming: FramebufferSerial is not "has the client ever described a framebuffer". + // MGPipeApplierReset ADVANCES every P4a working-state serial unconditionally + // (PipeApply.cpp), because a cleared window is itself a change a twin has to hear + // about - so the serial is non-zero after the first make-current even though no + // set_framebuffer_state has ever been applied, and gating on it would take this arm + // with two empty records and leave every framebuffer unsynced. The record's own Fbo + // handle is the test, and BOTH bindings have to be described before this arm can run + // at all, because it answers for both in one pass: an emitter walks {Draw, Read} and a + // Target = Both record writes both, so a half-described applier is the transitional + // state of a tree whose client half has not landed yet, and that state belongs to the + // pre-handle arm. + static Bool SyncCurrentFBOByRecord() { + const auto& st = MG_Pipe::MGPipeApplier(); + if (MG_Pipe::MGPipeHandleIsNull(st.DrawFramebuffer.Fbo) || + MG_Pipe::MGPipeHandleIsNull(st.ReadFramebuffer.Fbo)) { + return false; + } + + const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read}; + for (auto& target : fboTargets) { + if (SyncedFramebufferSerialIsCurrent(target, st.FramebufferSerial)) continue; + + const MG_Pipe::MGPFramebufferState& record = target == FramebufferTarget::Draw + ? st.DrawFramebuffer + : st.ReadFramebuffer; + if (record.IsDefault != 0) { + // The default framebuffer, said by the record rather than by comparing the + // bound object against pDefaultFramebufferInfo->defaultFBO - which is one + // of the four identity comparisons the reserved handle {0,1} exists to + // retire (MGPipeHandles.h). Nothing to sync except the widened-attachment + // masks, which only SyncToBackend ever writes and would otherwise still + // describe the user FBO that was draw-bound before. The window surface is a + // real RGBA buffer, so nothing here is ever widened. + if (target == FramebufferTarget::Draw) { + g_alphaWidenedDrawBufferMask = 0; + g_integerColorDrawBufferMask = 0; + } + StampSyncedFramebufferSerial(target, st.FramebufferSerial); + continue; + } + + // MONOLITH GLUE, and named as such: the twin's SyncToBackend still reads the + // frontend object (package D re-keys its BODY onto the record; this package + // owns the call site and the decisions around it). The IDENTITY is already the + // handle - FindByHandle(record.Fbo) - so a recycled FBO cannot be mistaken for + // its predecessor here; the resolve-or-create fallback below is what becomes + // GetOrCreate(record.Fbo) once package D forwards that overload. + auto& slot = GetFramebufferBindingSlotChecked(target); + const auto& currentFBO = slot.GetBoundObject(); + if (!currentFBO) { + MGLOG_E_ONCE("A framebuffer record names %s but no FBO is bound to it.", + target == FramebufferTarget::Read ? "READ" : "DRAW"); + continue; + } + + auto* twinSlot = g_backendFramebufferObjects.FindByHandle(record.Fbo); + auto& backendObj = + twinSlot ? *twinSlot : g_backendFramebufferObjects.GetOrCreate(currentFBO); + if (!backendObj) { + backendObj = MakeShared(); + } + + // THE "SAME FBO AS DRAW" SKIP IS NOW A FIELD, not a pointer comparison against + // the object the previous iteration happened to sync. Target = Both is the + // client saying one object is bound to both bindings, so the attachment and + // draw-buffer work the DRAW pass already did is not repeated - and the read + // buffer, which is READ-target-specific and is what that skip used to drop, is + // applied from the record's OWN ReadSurface. That is what makes the + // read-buffer-shared-FBO defect class unrepresentable rather than merely fixed: + // there is no path here that can reach the read binding without its own + // resolved read surface. + if (target == FramebufferTarget::Read && + record.Target == static_cast(MG_Pipe::MGPipeFramebufferTarget::Both)) { + backendObj->SyncReadBufferToBackend(currentFBO); + StampSyncedFramebufferSerial(target, st.FramebufferSerial); + continue; + } + + backendObj->SyncToBackend(currentFBO, target); + StampSyncedFramebufferSerial(target, st.FramebufferSerial); + } + + return true; + } +#endif // MOBILEGL_PIPE_PUSH + void SyncCurrentFBO() { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -2234,6 +2436,12 @@ namespace MobileGL::MG_Backend::DirectGLES { TextureImpl::g_backendTextureObjects.CollectGarbageIfNeeded(); RenderbufferImpl::g_backendRenderbufferObjects.CollectGarbageIfNeeded(); +#if MOBILEGL_PIPE_PUSH + // The handle arm first, and it declines rather than half-running: with no record + // yet it hands the walk straight back to the pre-handle arm below, unchanged. + if (EsprytDrawFramebufferHandlesEnabled() && SyncCurrentFBOByRecord()) return; +#endif + const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read}; MG_State::GLState::FramebufferObject* lastUpdatedFBO = nullptr; @@ -3047,6 +3255,30 @@ namespace MobileGL::MG_Backend::DirectGLES { static Bool g_broadcastMemoValid = false; static Uint g_broadcastMemoCount = 1; +#if MOBILEGL_PIPE_PUSH + // P4a e1 (D-C4). The handle arm's key is the framebuffer record's ContentHash, and it + // is a COMPLETE key for this answer rather than a cheaper approximation of the trio + // above: the hash is taken over every field the record carries - Fbo included, so a + // recycled framebuffer handle cannot be suppressed against its predecessor's record, + // and DrawBuffers[8] included, so an unchanged hash provably means the draw-buffer + // array did not move, which provably means the broadcast count did not move. + // + // The ORDERING that made this read come from the frontend is preserved and is what + // makes reading a record legal here: the client emits every set_* BEFORE the verb and + // the server specialises AT the verb, so by the time this runs the record already + // describes the framebuffer this draw will use - which is exactly the property the + // frontend read was buying (a program compiled against a stale count would otherwise + // not be relinked until the draw after the one that needed it). + // + // Separate statics rather than reusing the trio's: FramebufferSerial goes from 0 to + // non-zero exactly once in a process's life, so both arms can run in one run, and one + // set of fields carrying two key shapes is how a stale half is compared against a live + // one. + static Uint64 g_broadcastMemoContentHash = 0; + static Bool g_broadcastMemoHandleValid = false; + static Uint g_broadcastMemoHandleCount = 1; +#endif + // The identity+version key above is only monotonic WITHIN one GLContext: a // library teardown + re-init frees every FramebufferObject and restarts the // draw slot's counter at zero, so a recycled FBO address with coinciding @@ -3054,6 +3286,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // structurally identical SyncCurrentFBO trio (InvalidateFramebufferBindingCache). void InvalidateBroadcastMemo() { g_broadcastMemoValid = false; +#if MOBILEGL_PIPE_PUSH + g_broadcastMemoHandleValid = false; +#endif } void SyncCurrentProgram(const SharedPtr& currentProgram) { @@ -3077,28 +3312,55 @@ namespace MobileGL::MG_Backend::DirectGLES { // only runs later in PrepareForDraw: a program compiled against a stale count // would not be relinked until the draw after the one that needed it. { - const auto& drawSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Draw); - const auto& drawFBO = drawSlot.GetBoundObject(); - const Uint16 slotVersion = drawSlot.GetVersion(); - const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0; - if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() || - g_broadcastMemoSlotVersion != slotVersion || g_broadcastMemoObjectVersion != objectVersion) { - Uint enabledDrawBuffers = 0; - if (drawFBO) { - const auto& drawBuffers = drawFBO->GetDrawBuffers(); - for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { - if (drawBuffers[i] != FramebufferAttachmentType::None) { - enabledDrawBuffers = i + 1; + Bool broadcastCountResolved = false; +#if MOBILEGL_PIPE_PUSH + if (EsprytDrawFramebufferHandlesEnabled()) { + const auto& st = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPFramebufferState& record = st.DrawFramebuffer; + if (!MG_Pipe::MGPipeHandleIsNull(record.Fbo)) { + if (!g_broadcastMemoHandleValid || g_broadcastMemoContentHash != record.ContentHash) { + Uint enabledDrawBuffers = 0; + for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { + // -1 is FramebufferAttachmentType::None on the wire; every + // other value is an attachment index. + if (record.DrawBuffers[i] >= 0) { + enabledDrawBuffers = i + 1; + } + } + g_broadcastMemoContentHash = record.ContentHash; + g_broadcastMemoHandleCount = std::max(enabledDrawBuffers, 1); + g_broadcastMemoHandleValid = true; + } + g_fragColorBroadcastCount = g_broadcastMemoHandleCount; + broadcastCountResolved = true; + } + } +#endif + if (!broadcastCountResolved) { + const auto& drawSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Draw); + const auto& drawFBO = drawSlot.GetBoundObject(); + const Uint16 slotVersion = drawSlot.GetVersion(); + const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0; + if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() || + g_broadcastMemoSlotVersion != slotVersion || + g_broadcastMemoObjectVersion != objectVersion) { + Uint enabledDrawBuffers = 0; + if (drawFBO) { + const auto& drawBuffers = drawFBO->GetDrawBuffers(); + for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { + if (drawBuffers[i] != FramebufferAttachmentType::None) { + enabledDrawBuffers = i + 1; + } } } + g_broadcastMemoFbo = drawFBO.get(); + g_broadcastMemoSlotVersion = slotVersion; + g_broadcastMemoObjectVersion = objectVersion; + g_broadcastMemoCount = std::max(enabledDrawBuffers, 1); + g_broadcastMemoValid = true; } - g_broadcastMemoFbo = drawFBO.get(); - g_broadcastMemoSlotVersion = slotVersion; - g_broadcastMemoObjectVersion = objectVersion; - g_broadcastMemoCount = std::max(enabledDrawBuffers, 1); - g_broadcastMemoValid = true; + g_fragColorBroadcastCount = g_broadcastMemoCount; } - g_fragColorBroadcastCount = g_broadcastMemoCount; } BackendProgramObjectImpl* twin = nullptr; @@ -3207,6 +3469,47 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif +#if MOBILEGL_PIPE_PUSH + // The handle arm reads the record instead of the binding slot, which is one of the + // five GetFramebufferBindingSlotChecked call sites this phase retires. Everything the + // pre-handle arm below is careful about is careful here for the same reasons and in + // the same order: NO FAST PATH on any version (the skip is BindFramebufferId's job and + // its shadow is where the cost actually is), and the DEFAULT framebuffer is bound + // THROUGH THE SHADOW, never raw. What changes is only where the two questions - "which + // framebuffer" and "is it the default one" - are answered: the record's Fbo and its + // IsDefault byte, rather than the bound object's address and a comparison against + // pDefaultFramebufferInfo->defaultFBO. + if (EsprytDrawFramebufferHandlesEnabled()) { + const auto& st = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPFramebufferState& record = + target == FramebufferTarget::Draw ? st.DrawFramebuffer : st.ReadFramebuffer; + // The record's own handle is the "has this binding ever been described" test, not + // FramebufferSerial - which MGPipeApplierReset advances whether or not anything + // was ever emitted (see SyncCurrentFBOByRecord). + if (!MG_Pipe::MGPipeHandleIsNull(record.Fbo)) { + if (record.IsDefault == 0) { + auto* twinEntry = FramebufferImpl::g_backendFramebufferObjects.FindByHandle(record.Fbo); + if (twinEntry && *twinEntry) { + (*twinEntry)->Bind(target); + } else { + MGLOG_E_ONCE( + "No backend FBO found (maybe not synced) for the current %s FBO record, " + "cannot bind FBO.", + (target == FramebufferTarget::Read ? "READ" : "DRAW")); + } + } else { + MGLOG_D("Binding default framebuffer as %s FBO", + (target == FramebufferTarget::Read ? "READ" : "DRAW")); + // Through the shadow: a raw bind here would leave the shadow claiming + // the previous user FBO, false-skipping its next re-bind. + FramebufferImpl::BindFramebufferId( + target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0); + } + return; + } + } +#endif + auto& slot = GetFramebufferBindingSlotChecked(target); // No fast path on the binding slot's version. It is a 16-bit counter that only // ForceBindCurrentFBO ever stamps here, so the comparison was against an arbitrarily old @@ -3305,6 +3608,14 @@ namespace MobileGL::MG_Backend::DirectGLES { FramebufferImpl::g_fboSyncedObjects[(SizeT)target] = fbo.get(); FramebufferImpl::g_fboSyncedBackendIdGenerations[(SizeT)target] = FramebufferImpl::g_attachmentBackendIdGeneration; +#if MOBILEGL_PIPE_PUSH + // The handle arm's equivalent, and it is stamped per target for exactly the reason the + // memo is kept per target at all: this entry point syncs ONE binding and must not + // claim the other one is current too. + if (EsprytDrawFramebufferHandlesEnabled()) { + FramebufferImpl::StampSyncedFramebufferSerial(target, MG_Pipe::MGPipeApplier().FramebufferSerial); + } +#endif } static void BindCurrentProgramWithResources( @@ -5595,6 +5906,9 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); FramebufferImpl::InvalidateFramebufferBindingCache(); +#if MOBILEGL_PIPE_PUSH + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); +#endif if (!resolved) { MGLOG_E_ONCE("BlitFramebuffer: multisample resolve fallback failed"); } @@ -6110,6 +6424,9 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); FramebufferImpl::InvalidateFramebufferBindingCache(); +#if MOBILEGL_PIPE_PUSH + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); +#endif if (!ok) { MGLOG_E_ONCE("BlitFramebuffer: could not stage the source for the multisample replicate"); return false; @@ -6209,6 +6526,9 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); FramebufferImpl::InvalidateFramebufferBindingCache(); +#if MOBILEGL_PIPE_PUSH + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); +#endif } if (readSamples <= 0 && drawSamples > 0) { // Single-sample source into a multisample destination: ES rejects the call @@ -10131,6 +10451,9 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDrawFramebuffer)); } FramebufferImpl::InvalidateFramebufferBindingCache(); +#if MOBILEGL_PIPE_PUSH + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); +#endif // A driver that refuses the query leaves both at 0. Fall back to what the EGL config // was chosen with, which is what the surface actually has. @@ -10575,6 +10898,9 @@ namespace MobileGL::MG_Backend::DirectGLES { BufferImpl::InvalidateIndexedBufferBindingCache(); BufferImpl::InvalidatePixelBufferBindingCaches(); FramebufferImpl::InvalidateFramebufferBindingCache(); +#if MOBILEGL_PIPE_PUSH + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); +#endif PixelStoreImpl::InvalidatePackStateCache(); // The render-state shadow belongs in this list for the same reason as the ones above: // it describes the real ES context, which outlives the MobileGL context that is @@ -11108,6 +11434,9 @@ namespace MobileGL::MG_Backend::DirectGLES { ScratchFBOImpl::OnBackendContextDestroyed(); ReleasePackedWordScratchTexture(); FramebufferImpl::InvalidateFramebufferBindingCache(); +#if MOBILEGL_PIPE_PUSH + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); +#endif VertexArrayImpl::InvalidateVAOBindingCache(); PixelStoreImpl::InvalidatePackStateCache(); PrgramImpl::InvalidateBroadcastMemo();