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};