From f5761ea1f30770bbe6ffa96ba15fb513ba0a211a Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Thu, 6 Aug 2026 10:35:41 -0400 Subject: [PATCH] [Perf] (MG_Backend): diff only the render-state span that moved, and gate the per-draw walks Four per-draw costs in DirectGLES, all of the same species: work re-done for an answer that had not changed. SyncRenderState was guarded by a single version compare, so one blend toggle - the way Blaze3D brackets every batch - re-diffed the whole ~40-field render state block and copied the full struct back into the shadow, every draw. The parameter struct is now split into three contiguous byte spans, each gated by a memcmp against the backend shadow; a per-draw blend flip touches only the blend span. The shadow is byte-cloned after each sync so the span compares stay exact, padding included. Blocks whose inputs live outside the parameter struct (the surface-size viewport fallback, the sRGB context capability) stay ungated, and the dual-source-blend hard-fail still fires every draw because a throwing sync never stamps the shadow. SyncMipmapsToBackend gained a first-level clean gate on (context id, sampling-resolution generation, content version, params version) that skips the IsComplete walk and the eight-field shape probe outright; every shape mutation funnels through BumpShapeVersion, which is what makes the gate sound. SyncToBackend for vertex arrays compares one aggregate config version instead of three stamps per attribute slot. And SyncNeccessaryTextures memoises the draw-framebuffer attachment list, keyed the same way the framebuffer sync memo already is, instead of re-walking attachments per draw. ns per draw, DriverBench on a GTX 1660 SUPER, isolated A/B: mc_state_toggle 3151 -> 2397, mc_ubo_range 792 -> 579, mc_vanilla_draw 1111 -> 881, mc_sampler_churn 2019 -> 1676, mc_use_program 5132 -> 4356; every one of the nine cases improved. Against the native driver Espryt now stands at 3.6x on the plain draw path, 2.8x on the per-draw uniform-range path and 2.1x on the blend toggle, from 8.7x / 9.1x / 7.2x when this effort began. Unit tests 421/421. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 166 ++++++++++++++---- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 59 ++++++- MobileGL/MG_Backend/DirectGLES/Managers.h | 22 +++ 3 files changed, 207 insertions(+), 40 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 13a509d8..3b1e42da 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -858,6 +858,16 @@ namespace MobileGL::MG_Backend::DirectGLES { static Int g_unitTextureSyncListMaxUnit = -1; static Uint g_unitTextureSyncListContextGeneration = 0; + // Sibling memo for the draw FBO's texture attachments (see the use site in + // SyncNeccessaryTextures for the key derivation and the borrow rules, which are the + // unit list's). A null FBO pointer means "not stamped". + static Vector g_fboTextureSyncList; + static MG_State::GLState::FramebufferObject* g_fboTextureSyncListFbo = nullptr; + static Uint16 g_fboTextureSyncListSlotVersion = 0; + static Uint16 g_fboTextureSyncListObjectVersion = 0; + static Uint64 g_fboTextureSyncListContextId = 0; + static Uint g_fboTextureSyncListContextGeneration = 0; + void SyncNeccessaryTextures() { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -904,16 +914,55 @@ namespace MobileGL::MG_Backend::DirectGLES { g_unitTextureSyncListValid = true; } - const auto& currentFBO = - MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); + // Texture attachments of the draw FBO, memoised like the unit list above: WHICH + // textures hang off the FBO only changes with a rebind (slot version), an + // attachment/draw-buffer edit (object version - the documented invariant + // SyncCurrentFBO's memo already leans on), a different FBO landing on a recycled + // heap address (pointer + slot version together, the StampSyncedFBO trio), another + // frontend context (context id) or a rebuilt ES context (backend generation). WHAT + // each texture then needs is still decided per draw by the version gates inside the + // three sync calls. Entry lifetime mirrors the unit list: the attachment holds the + // texture's SharedPtr at a stable address while the FBO is unchanged, and the + // registry keeps a backend object alive until its frontend texture expires, which an + // attached texture cannot. A renderbuffer-only FBO - the common Minecraft frame - + // reduces to the key compare and an empty loop. + const auto& drawSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw); + const auto& currentFBO = drawSlot.GetBoundObject(); if (currentFBO) { - for (const auto& attachment : currentFBO->GetAllAttachmentObjects()) { - if (!attachment.IsTexture()) continue; - auto& textureObject = attachment.GetTexture(); - if (textureObject) { - SyncTextureObjectToBackend(textureObject); + const Uint16 fboSlotVersion = drawSlot.GetVersion(); + const Uint16 fboObjectVersion = currentFBO->GetObjectVersion(); + const Bool fboListValid = + g_fboTextureSyncListFbo == currentFBO.get() && + g_fboTextureSyncListSlotVersion == fboSlotVersion && + g_fboTextureSyncListObjectVersion == fboObjectVersion && + g_fboTextureSyncListContextId == MG_State::pGLContext->GetTextureContextId() && + g_fboTextureSyncListContextGeneration == g_textureContextGeneration; + if (fboListValid) { + for (const auto& entry : g_fboTextureSyncList) { + entry.backend->SyncTextureParamsToBackend(*entry.slot); + entry.backend->SyncBuiltinSamplerToBackend(*entry.slot); + entry.backend->SyncMipmapsToBackend(*entry.slot); } + } else { + g_fboTextureSyncListFbo = nullptr; + g_fboTextureSyncList.clear(); + for (const auto& attachment : currentFBO->GetAllAttachmentObjects()) { + if (!attachment.IsTexture()) continue; + auto& textureObject = attachment.GetTexture(); + if (textureObject) { + g_fboTextureSyncList.push_back( + {&textureObject, SyncTextureObjectToBackend(textureObject).get()}); + } + } + g_fboTextureSyncListFbo = currentFBO.get(); + g_fboTextureSyncListSlotVersion = fboSlotVersion; + g_fboTextureSyncListObjectVersion = fboObjectVersion; + g_fboTextureSyncListContextId = MG_State::pGLContext->GetTextureContextId(); + g_fboTextureSyncListContextGeneration = g_textureContextGeneration; } + } else { + g_fboTextureSyncListFbo = nullptr; + g_fboTextureSyncList.clear(); } } @@ -1060,6 +1109,33 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& parameters = MG_State::pGLContext->GetRenderStateParameters(); + // The frontend has ONE version for the whole parameter block, so a per-draw blend + // toggle used to re-diff all ~40 pieces of state field by field on every draw + // (Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), making this the + // hottest thing mc_state_toggle did). Split the struct into three contiguous byte + // spans - the head (viewport/point/line/polygon-offset scalars), the blend array, + // and everything after it - and let one memcmp per span decide whether its blocks + // run at all. memcmp can false-DIFFER on padding bytes (harmless: the field-wise + // block runs and finds nothing) but can never false-match, and after the first full + // sync the tail memcpy below makes the shadow byte-identical, padding included, so + // in the steady state a span memcmp is exact. Blocks whose inputs are NOT in the + // parameter struct (the surface-size viewport fallback, the sRGB context + // capability) stay outside the gates. + static_assert(std::is_trivially_copyable_v, + "span memcmp/memcpy below treats the parameter block as raw bytes"); + constexpr SizeT kBlendSpanBegin = offsetof(RenderStateParameters, BlendStates); + constexpr SizeT kBlendSpanEnd = offsetof(RenderStateParameters, LogicOp); + const auto* currentBytes = reinterpret_cast(¶meters); + const auto* syncedBytes = reinterpret_cast(&g_syncedRenderStateParameters); + const Bool headSpanDirty = + !g_hasSyncedRenderState || std::memcmp(currentBytes, syncedBytes, kBlendSpanBegin) != 0; + const Bool blendSpanDirty = + !g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanBegin, syncedBytes + kBlendSpanBegin, + kBlendSpanEnd - kBlendSpanBegin) != 0; + const Bool tailSpanDirty = + !g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd, + sizeof(RenderStateParameters) - kBlendSpanEnd) != 0; + IntVec4 backendViewport = parameters.Viewport; if (backendViewport.z() <= 0 || backendViewport.w() <= 0) { Int surfaceWidth = 0; @@ -1074,6 +1150,8 @@ namespace MobileGL::MG_Backend::DirectGLES { g_syncedBackendViewport = backendViewport; } + // All 12 capability bools live after LogicOp in the struct, i.e. in the tail span. + if (tailSpanDirty) { #define SYNC_CAPABILITY(cap_mg, cap_gl) \ if (parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \ if (parameters.cap_mg##Enabled) { \ @@ -1082,20 +1160,21 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glDisable(cap_gl); \ } \ } - SYNC_CAPABILITY(DepthTest, GL_DEPTH_TEST); - SYNC_CAPABILITY(ColorLogicOp, GL_COLOR_LOGIC_OP); - SYNC_CAPABILITY(Dither, GL_DITHER); - SYNC_CAPABILITY(Multisample, GL_MULTISAMPLE); - SYNC_CAPABILITY(SampleAlphaToCoverage, GL_SAMPLE_ALPHA_TO_COVERAGE); - SYNC_CAPABILITY(SampleCoverage, GL_SAMPLE_COVERAGE); - SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK); - SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL); - SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD); - SYNC_CAPABILITY(ScissorTest, GL_SCISSOR_TEST); - SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST); - SYNC_CAPABILITY(CullFace, GL_CULL_FACE); + SYNC_CAPABILITY(DepthTest, GL_DEPTH_TEST); + SYNC_CAPABILITY(ColorLogicOp, GL_COLOR_LOGIC_OP); + SYNC_CAPABILITY(Dither, GL_DITHER); + SYNC_CAPABILITY(Multisample, GL_MULTISAMPLE); + SYNC_CAPABILITY(SampleAlphaToCoverage, GL_SAMPLE_ALPHA_TO_COVERAGE); + SYNC_CAPABILITY(SampleCoverage, GL_SAMPLE_COVERAGE); + SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK); + SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL); + SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD); + SYNC_CAPABILITY(ScissorTest, GL_SCISSOR_TEST); + SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST); + SYNC_CAPABILITY(CullFace, GL_CULL_FACE); #undef SYNC_CAPABILITY + } { // sRGB framebuffer writes. GLES core always encodes a write into an sRGB attachment, // while GL_FRAMEBUFFER_SRGB is disabled by default in desktop GL and the frontend @@ -1110,8 +1189,8 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Primitive restart. GLES core has only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed all-ones - // value); both the fixed cap and the (fixed-valued) arbitrary GL_PRIMITIVE_RESTART map to + if (tailSpanDirty) { // Primitive restart. GLES core has only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed + // all-ones value); both the fixed cap and the (fixed-valued) arbitrary GL_PRIMITIVE_RESTART map to // it. An arbitrary non-fixed restart index is rejected at draw time (see DrawElements). const Bool restart = parameters.PrimitiveRestartFixedIndexEnabled || parameters.PrimitiveRestartEnabled; const Bool syncedRestart = g_syncedRenderStateParameters.PrimitiveRestartFixedIndexEnabled || @@ -1124,7 +1203,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; }; - { // Blend State + if (blendSpanDirty) { // Blend State using FBO = MG_State::GLState::FramebufferObject; const auto& targetStates = parameters.BlendStates; auto& syncedStates = g_syncedRenderStateParameters.BlendStates; @@ -1288,7 +1367,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Depth state + if (tailSpanDirty) { // Depth state if (parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) { g_GLESFuncs.glDepthFunc(MG_Util::ConvertDepthTestFuncToGLEnum(parameters.DepthFunc)); } @@ -1300,7 +1379,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Stencil state + if (tailSpanDirty) { // Stencil state for (SizeT faceIndex = 0; faceIndex < parameters.StencilStates.size(); ++faceIndex) { const StencilFaceState& current = parameters.StencilStates[faceIndex]; const StencilFaceState& synced = g_syncedRenderStateParameters.StencilStates[faceIndex]; @@ -1325,7 +1404,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Color mask. Uniform masks use the non-indexed glColorMask (works everywhere); divergent + if (tailSpanDirty) { // Color mask. Uniform masks use the non-indexed glColorMask (works everywhere); divergent // per-draw-buffer masks use the indexed glColorMaski when draw_buffers_indexed is // available, otherwise fall back to broadcasting draw buffer 0. Mirrors the blend block. using FBO = MG_State::GLState::FramebufferObject; @@ -1359,7 +1438,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present. + if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present. // Without the extension the mode stays FILL and non-FILL requests are dropped. if (parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront && g_GLESCapabilities.SupportsPolygonMode) { @@ -1369,7 +1448,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Clear values + if (tailSpanDirty) { // Clear values if (parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) { const FloatVec4& clearCol = parameters.ClearColor; g_GLESFuncs.glClearColor(clearCol.x(), clearCol.y(), clearCol.z(), clearCol.w()); @@ -1386,7 +1465,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Cull face mode + if (tailSpanDirty) { // Cull face mode if (parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) { const CullFaceMode& cfm = parameters.CullFaceModeSetting; g_GLESFuncs.glCullFace(MG_Util::ConvertCullFaceModeToGLEnum(cfm)); @@ -1397,39 +1476,39 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Scissor box + if (tailSpanDirty) { // Scissor box if (parameters.ScissorBox != g_syncedRenderStateParameters.ScissorBox) { const IntVec4& scissorBox = parameters.ScissorBox; g_GLESFuncs.glScissor(scissorBox.x(), scissorBox.y(), scissorBox.z(), scissorBox.w()); } } - { // Logic op + if (tailSpanDirty) { // Logic op (first field of the tail span) if (parameters.LogicOp != g_syncedRenderStateParameters.LogicOp) { g_GLESFuncs.glLogicOp(MG_Util::ConvertLogicOperationToGLEnum(parameters.LogicOp)); } } - { // Polygon offset + if (headSpanDirty) { // Polygon offset (head-span scalars, like line width / point size below) if (parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor || parameters.PolygonOffsetUnits != g_syncedRenderStateParameters.PolygonOffsetUnits) { g_GLESFuncs.glPolygonOffset(parameters.PolygonOffsetFactor, parameters.PolygonOffsetUnits); } } - { // Line width + if (headSpanDirty) { // Line width if (parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) { g_GLESFuncs.glLineWidth(parameters.LineWidth); } } - { // Point size + if (headSpanDirty) { // Point size if (parameters.PointSize != g_syncedRenderStateParameters.PointSize) { g_GLESFuncs.glPointSize(parameters.PointSize); } } - { // Sample coverage + if (tailSpanDirty) { // Sample coverage if (parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue || parameters.SampleCoverageInvert != g_syncedRenderStateParameters.SampleCoverageInvert) { g_GLESFuncs.glSampleCoverage(parameters.SampleCoverageValue, @@ -1437,14 +1516,29 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { // Sample mask + if (tailSpanDirty) { // Sample mask if (g_GLESFuncs.glSampleMaski && parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue) { g_GLESFuncs.glSampleMaski(0, parameters.SampleMaskValue); } } g_syncedRenderStateVersion = currentRenderStateVersion; - g_syncedRenderStateParameters = parameters; + // Byte copy, not member copy: it also clones the frontend struct's padding bytes, + // which is what lets the span memcmps above answer "unchanged" exactly instead of + // tripping on indeterminate padding every draw. Only the dirty spans need copying - + // a clean span's bytes are already identical by the very memcmp that skipped it. + auto* syncedBytesMut = reinterpret_cast(&g_syncedRenderStateParameters); + if (headSpanDirty) { + std::memcpy(syncedBytesMut, currentBytes, kBlendSpanBegin); + } + if (blendSpanDirty) { + std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin, + kBlendSpanEnd - kBlendSpanBegin); + } + if (tailSpanDirty) { + std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd, + sizeof(RenderStateParameters) - kBlendSpanEnd); + } g_hasSyncedRenderState = true; } } // namespace RenderStateImpl diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index ae0df61a..56255bf5 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1348,11 +1348,24 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Syncing VAO with backend ID %u to backend for state ID %u", m_backendVAOId, stateVAOObject->GetExternalIndex()); + // One compare instead of MAX_VERTEX_ATTRIBS x 3 per draw: the config version + // aggregates every per-attribute version bump (see the member comment), and the + // index-buffer slot version covers the only other thing this function reads. When + // both are clean there is nothing to emit, and the VAO is not even bound here - + // PrepareForDraw's BindCurrentVAO establishes the draw binding regardless. + const Uint32 currentConfigVersion = stateVAOObject->GetConfigVersion(); + const Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion(); + const Bool attributesDirty = !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion; + const Bool indexBufferDirty = currentIndexBufferVersion != m_syncedIndexBufferVersion; + if (!attributesDirty && !indexBufferDirty) { + return; + } + Bind(); const auto& allAttributeVersions = stateVAOObject->GetAllAttributeVersions(); const auto& allAttributes = stateVAOObject->GetAllAttributes(); - for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) { + for (Uint attribIndex = 0; attribIndex < allAttributes.size() && attributesDirty; ++attribIndex) { const auto& attrib = allAttributes[attribIndex]; Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion != m_syncedAttributeVersions[attribIndex].SwitchVersion; @@ -1407,8 +1420,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion(); - if (currentIndexBufferVersion != m_syncedIndexBufferVersion) { + if (indexBufferDirty) { const auto& indexBufferBinding = stateVAOObject->GetIndexBufferBindingSlot().GetBoundObject(); Bool indexBufferSynced = false; if (indexBufferBinding) { @@ -1429,7 +1441,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - m_syncedAttributeVersions = allAttributeVersions; + if (attributesDirty) { + m_syncedAttributeVersions = allAttributeVersions; + m_syncedConfigVersion = currentConfigVersion; + m_hasSyncedConfigVersion = true; + } } void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays( @@ -1826,6 +1842,23 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + // First-level clean gate (see the member comment): three version compares and no + // virtual shape walk. Every mutation the slower probe below would catch bumps one of + // the keys - shape via the context's sampling-resolution generation (coarse: any + // texture's shape churn re-opens every gate, which only costs a fall-through to the + // probe), CPU pixels via the content version, samples/fixed-locations via the params + // version - and backend-side storage resets clear m_isInitialized. Restricted to + // Mipmap storage like the probe fast path: a buffer texture's backing store can move + // without any of these keys noticing. + if (m_isInitialized && m_syncedShapeContextId != 0 && MG_State::pGLContext && + m_syncedShapeContextId == MG_State::pGLContext->GetTextureContextId() && + m_syncedShapeGeneration == MG_State::pGLContext->GetSamplingResolutionGeneration() && + m_syncedContentVersion == stateTextureObject->GetContentVersion() && + m_syncedShapeParamsVersion == stateTextureObject->GetTextureParamsVersion() && + stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) { + return; + } + MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); @@ -1879,6 +1912,14 @@ namespace MobileGL::MG_Backend::DirectGLES { if (probe == m_prevTextureInfo) { MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.", m_backendTextureId); + // The probe just proved "fully synced" from the real state, so the cheap + // gate may be (re)stamped here: the coarse generation only ever goes stale + // from OTHER textures' churn, and this draw re-validated this one. + if (MG_State::pGLContext) { + m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId(); + m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration(); + m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion(); + } return; } } @@ -2412,6 +2453,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // upload); stamp the version so per-draw re-syncs short-circuit until // the next CPU-side mutation. m_syncedContentVersion = stateTextureObject->GetContentVersion(); + // Same instant, so the cheap gate's keys describe exactly this synced state. + // Only Mipmap storage may arm it - the gate refuses other storage types anyway, + // but a stale trio must not linger on an object that later switches type. + if (MG_State::pGLContext && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) { + m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId(); + m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration(); + m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion(); + } else { + m_syncedShapeContextId = 0; + } } void BackendTextureObject::SyncBuiltinSamplerToBackend( diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index c720084b..1111eb26 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -260,6 +260,13 @@ namespace MobileGL::MG_Backend::DirectGLES { Array m_clientAttributeBufferIds; Bool m_isInitialized = false; Uint16 m_syncedIndexBufferVersion = 0; + // Aggregate gate over the per-attribute walk below: the frontend bumps its config + // version on every per-attribute version bump (the three Bump*Version functions are + // its only writers), so an unchanged config version proves every per-attribute + // compare in SyncToBackend would come up clean. The index-buffer slot has its own + // version and is NOT covered. The Bool (not a sentinel value) marks "never synced". + Bool m_hasSyncedConfigVersion = false; + Uint32 m_syncedConfigVersion = 0; Array m_syncedAttributeVersions; }; @@ -388,6 +395,21 @@ namespace MobileGL::MG_Backend::DirectGLES { // clean probe compares this before rebuilding shape info and scanning // per-level dirty flags; 0 never matches a real version (they start at 1). Uint64 m_syncedContentVersion = 0; + // First-level clean gate for SyncMipmapsToBackend, checked before even the + // IsComplete()/shape-probe walk. Valid only as a trio with the content and + // texture-params versions: the context's sampling-resolution generation moves on + // EVERY texture-shape mutation (BumpShapeVersion is the only writer of shape and + // unconditionally bumps it), the content version on every CPU pixel mutation, and + // the params version covers SetSamples/SetFixedSampleLocations, which bump neither + // of the other two but feed the shape probe. The context id pins the generation to + // the context that produced it - generations restart at 0 with a new context, and a + // texture is owned by exactly one context (share groups are not implemented), so a + // mutation can never happen under a context this key does not name. 0 = never + // stamped (real context ids start at 1). Backend-side invalidation rides on + // m_isInitialized: RequireImageBindableStorage and RecreateBackendTexture clear it. + Uint64 m_syncedShapeContextId = 0; + Uint64 m_syncedShapeGeneration = 0; + Uint16 m_syncedShapeParamsVersion = 0; SamplerParameters m_cacheSamplerParameters; UintVec2 m_cacheLodRange = {0, 1000}; FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};