From 49aab57f031f53fc52d708f917823790f1a23281 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Thu, 6 Aug 2026 07:41:10 -0400 Subject: [PATCH] [Perf] (MG_Backend, MG_State): stop re-resolving texture unit bindings on every draw DirectGLES re-derived the whole texture binding state for every draw: for each touched unit, two alias-resolution passes over all binding slots, then a third walk to unbind native targets nothing claimed, then the sampler. With the Minecraft-shaped bench that was 13.2% of the render thread in BindCurrentTextures alone, plus 4.6% in SyncNeccessaryTextures deciding which textures to consider. The answer is identical across a whole terrain batch. The resolution is now memoised, and what makes replaying it as a no-op legitimate is that the memo does not merely trust a key: it compares the backend's own bound texture shadow against the one resolution left behind. Every path that binds a texture behind this function's back already maintains that shadow - the scratch bind an upload does on the temp unit, CopyTexSubImage2D and GenerateMipmap binding on the active unit, the glBindTextures fast path, the scrub a backend texture performs when it is destroyed or respecified - so a memcmp catches all of them without having to enumerate them. On top of that the key covers the texture bind generation, the program that arbitrates aliased targets (pointer, lifetime id, backend state version, link status), and the ES context generation. Two invalidation sources had no signal at all and needed one. Mipmap completeness decides whether a texture is bound in the first place, and it moves with texture shape and with the effective sampler's filter - so a sampling-resolution generation now moves with both, routed through single choke points (TextureObjectBase::BumpShapeVersion, SamplerObject::BumpVersion) so a future bump site cannot forget it. A texture context id was needed because both generations restart at zero in a new GLContext, which can land on the old heap address. This also closes a pre-existing hole rather than working around it: glDeleteSamplers unbinds the sampler from every unit straight through TextureUnit::SetSamplerObject, bypassing the touch bookkeeping, so that setter now bumps the bind generation on a real change. The sampler bind step itself stays outside the memo and runs every draw - the program's raw-depth-fetch substitution rewrites unit samplers immediately afterwards, so a memo there could never hit. ns per draw, DriverBench on a GTX 1660 SUPER (native / Espryt): mc_vanilla_draw 253 / 2037->1315, mc_ubo_range 202 / 1684->955, mc_sodium_multidraw 739 / 3939->3150. Espryt goes from 8.3x to 4.7x the native driver on the per-draw uniform-range case. Magma is unaffected (the MG_State additions are counter bumps), and no case regressed. Unit tests 421/421. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 202 ++++++++++++++++-- MobileGL/MG_State/GLState/Core.h | 12 ++ .../GLState/SamplerState/SamplerObject.cpp | 40 ++-- .../GLState/SamplerState/SamplerObject.h | 6 + .../GLState/TextureState/TextureObject.cpp | 22 +- .../GLState/TextureState/TextureObject.h | 6 + .../TextureState/TextureObject2DCube.cpp | 4 +- .../GLState/TextureState/TextureState.cpp | 10 +- .../GLState/TextureState/TextureState.h | 25 +++ .../GLState/TextureState/TextureUnit.cpp | 12 ++ 10 files changed, 294 insertions(+), 45 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index e93c95ad..f301e5db 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -813,6 +813,33 @@ namespace MobileGL::MG_Backend::DirectGLES { return backendObj; } + // Work list behind SyncNeccessaryTextures' per-draw unit walk. WHICH textures the + // touched units hold is a pure function of the unit bindings, so the GLContext identity + // (a never-reused id, not the heap address a recreated context can land on again), the + // texture bind generation and the touched-unit high-water mark are a complete key - + // WHAT each entry then has to do is still decided per draw by the version compares + // inside the sync calls, which is why texture content, shape and parameter changes need + // no key here. + // + // Entries borrow, they never own. `slot` points at the binding slot's shared_ptr, whose + // address is fixed for the context's lifetime (TextureState holds the unit array by + // value) and whose VALUE cannot change without bumping the bind generation. `backend` is + // the registry's object for the texture in that slot; the registry only erases an entry + // once the frontend texture has expired, and a frontend texture cannot expire while a + // slot the key covers still holds a reference to it. Holding either side by shared_ptr + // instead would keep dead frontend textures alive and defeat the registry's + // weak-reference GC. + struct UnitTextureSyncEntry { + const SharedPtr* slot = nullptr; + BackendTextureObject* backend = nullptr; + }; + static Vector g_unitTextureSyncList; + static Bool g_unitTextureSyncListValid = false; + static Uint64 g_unitTextureSyncListContextId = 0; + static Uint64 g_unitTextureSyncListBindGeneration = 0; + static Int g_unitTextureSyncListMaxUnit = -1; + static Uint g_unitTextureSyncListContextGeneration = 0; + void SyncNeccessaryTextures() { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -826,16 +853,37 @@ namespace MobileGL::MG_Backend::DirectGLES { // Units past the frontend's high-water mark have provably-empty slots. const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit(); - for (Int index = 0; index <= maxTouchedUnit; ++index) { - auto& unit = MG_State::pGLContext->GetTextureUnitObject(index); - for (const auto& bindingSlot : unit.GetAllBindingSlots()) { - auto& textureObject = bindingSlot.GetBoundObject(); - // An image-less default texture (name 0) is the slot's initial / "unbound" - // state; it has nothing to sync, so skip it as cheaply as the old null slot. - if (textureObject && !MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) { - SyncTextureObjectToBackend(textureObject); + const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration(); + if (g_unitTextureSyncListValid && + g_unitTextureSyncListContextId == MG_State::pGLContext->GetTextureContextId() && + g_unitTextureSyncListBindGeneration == bindGeneration && + g_unitTextureSyncListMaxUnit == maxTouchedUnit && + g_unitTextureSyncListContextGeneration == g_textureContextGeneration) { + for (const auto& entry : g_unitTextureSyncList) { + entry.backend->SyncTextureParamsToBackend(*entry.slot); + entry.backend->SyncBuiltinSamplerToBackend(*entry.slot); + entry.backend->SyncMipmapsToBackend(*entry.slot); + } + } else { + g_unitTextureSyncListValid = false; + g_unitTextureSyncList.clear(); + for (Int index = 0; index <= maxTouchedUnit; ++index) { + auto& unit = MG_State::pGLContext->GetTextureUnitObject(index); + for (const auto& bindingSlot : unit.GetAllBindingSlots()) { + auto& textureObject = bindingSlot.GetBoundObject(); + // An image-less default texture (name 0) is the slot's initial / "unbound" + // state; it has nothing to sync, so skip it as cheaply as the old null slot. + if (textureObject && !MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) { + g_unitTextureSyncList.push_back( + {&textureObject, SyncTextureObjectToBackend(textureObject).get()}); + } } } + g_unitTextureSyncListContextId = MG_State::pGLContext->GetTextureContextId(); + g_unitTextureSyncListBindGeneration = bindGeneration; + g_unitTextureSyncListMaxUnit = maxTouchedUnit; + g_unitTextureSyncListContextGeneration = g_textureContextGeneration; + g_unitTextureSyncListValid = true; } const auto& currentFBO = @@ -1532,21 +1580,19 @@ namespace MobileGL::MG_Backend::DirectGLES { XfbImpl::StartPendingTransformFeedback(); } - // Rebinds every frontend texture unit's textures (and sampler objects) on the - // backend context. Needed before draws AND compute dispatches: content syncs - // (SyncTextureObjectToBackend) bind scratch textures on the active unit as a - // side effect, so unit bindings must be re-established afterwards or shaders - // sample whatever texture the last sync left behind (e.g. Flywheel's depth - // pyramid downsample reading a stale unit-0 binding instead of the depth - // attachment). - void BindCurrentTextures() { + // Resolves every frontend texture unit's textures onto the backend context. Returns + // false when the resolution could not be completed from the state it read - a bound + // texture that has no backend object yet is skipped, and a later draw would bind it + // without any of the memo keys below moving - so the caller must not memoise it. + static Bool ResolveAndBindUnitTextures(const SharedPtr& currentProgram, + Int maxTouchedUnit) { #ifdef TRACY_ENABLE - ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND); + ZoneScopedNC("ResolveAndBindUnitTextures", TRACY_ZONECOLOR_BACKEND); #endif + Bool fullyResolved = true; // Frontend target the current program samples at a given unit; resolves an // aliased native binding when two real textures compete for it (see below). // Only consulted on a conflict, so the ordinary unit costs nothing. - const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); const auto sampledTargetForUnit = [¤tProgram](Int unit) { if (!currentProgram || !currentProgram->GetLinkStatus()) { return TextureTarget::Unknown; @@ -1562,8 +1608,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return TextureTarget::Unknown; }; - // Units past the frontend's high-water mark have provably-empty slots. - const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit(); for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); Array boundBackendTargets{}; @@ -1628,7 +1672,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // Bind texture object const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get()); - if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue; + if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) { + fullyResolved = false; + continue; + } backendTextureIt->second->Bind(targetGL, unit); boundBackendTargets[backendTargetIndex] = true; @@ -1655,9 +1702,19 @@ namespace MobileGL::MG_Backend::DirectGLES { TextureImpl::UnbindTexture(unit, targetGL); } } + } + return fullyResolved; + } - // Bind sampler object if necessary - const auto& samplerObject = textureUnit.GetSamplerObject(); + // Puts each touched unit's frontend sampler object on the backend unit. Deliberately + // NOT part of the memo below: BindCurrentProgramWithResources rewrites the sampler of + // every unit its program samples right after this runs (raw-depth-fetch substitution, + // per-program sampler objects), so the sampler shadow this leaves behind is not what + // the next call would find - a memo keyed on it could never hit. The step is a pointer + // compare per unit in the common case, far below the resolution cost the memo removes. + static void BindCurrentUnitSamplers(Int maxTouchedUnit) { + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { + const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject(); if (samplerObject) { const auto& backendSamplerIt = SamplerImpl::g_backendSamplerObjects.find(samplerObject.get()); if (backendSamplerIt != SamplerImpl::g_backendSamplerObjects.end()) { @@ -1672,6 +1729,105 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + // Memo of the resolved per-unit TEXTURE bindings, so a steady-state draw loop stops + // re-deriving an answer nothing has invalidated (a Minecraft frame issues thousands of + // draws that touch none of the inputs below, and the resolution walks every binding slot + // of every touched unit three times). + // + // ResolveAndBindUnitTextures is a pure function of: + // * the GLContext identity - a never-reused id, since the counters below restart at 0 in + // a new context and a recreated one can land on the old heap address; + // * its texture bind generation - glBindTexture, glBindTextureUnit, glBindTextures, + // glBindSampler, the delete-unbind in MarkTextureObjectForDeletion, the sampler swap in + // TextureUnit::SetSamplerObject (which is how glDeleteSamplers unbinds), and a default + // texture gaining or losing an image all bump it; + // * the touched-unit high-water mark (units above it have provably-empty slots); + // * the program that arbitrates two real textures aliased onto one native target - + // identity, plus the lifetime id because a freed program can be replaced at the same + // address, plus the backend-state version which moves on every relink and on every + // sampler-uniform unit assignment, plus the link status; + // * the sampling-resolution generation - any texture shape change or any sampler + // parameter change, i.e. everything mipmap-completeness is computed from, and + // completeness is what decides whether a texture is bound at all; + // * the ES context generation, because the backend texture ids and the driver's own + // binding state die with the context. + // + // A matching key only says the ANSWER is unchanged; replaying it as "do nothing" also + // requires the bindings it established to still be on the driver. Rather than enumerate + // every writer, the memo keeps the binding shadow it left and compares it: that shadow is + // already the authority every redundant-bind filter in this backend trusts, and every path + // that puts a texture on a unit behind this function's back maintains it - the upload + // path's scratch bind on the temp unit (BackendTextureObject::Bind out of + // SyncMipmapsToBackend), CopyTexSubImage2D and GenerateMipmap binding on the active unit, + // the glBindTextures fast path, and the self-scrub a BackendTextureObject performs when it + // is destroyed or respecified. + struct ResolvedTextureBindingMemo { + Bool valid = false; + Uint64 glContextId = 0; + Int maxTouchedUnit = -1; + Uint64 bindGeneration = 0; + Uint64 samplingResolutionGeneration = 0; + const void* program = nullptr; + Uint64 programLifetimeId = 0; + Uint32 programBackendStateVersion = 0; + Bool programLinked = false; + Uint contextGeneration = 0; + decltype(TextureImpl::g_boundTexturesCache) boundTextures{}; + }; + static ResolvedTextureBindingMemo g_resolvedTextureBindingMemo; + + // Rebinds every frontend texture unit's textures (and sampler objects) on the + // backend context. Needed before draws AND compute dispatches: content syncs + // (SyncTextureObjectToBackend) bind scratch textures on the active unit as a + // side effect, so unit bindings must be re-established afterwards or shaders + // sample whatever texture the last sync left behind (e.g. Flywheel's depth + // pyramid downsample reading a stale unit-0 binding instead of the depth + // attachment). + void BindCurrentTextures() { +#ifdef TRACY_ENABLE + ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND); +#endif + const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); + // Units past the frontend's high-water mark have provably-empty slots. + const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit(); + + auto& memo = g_resolvedTextureBindingMemo; + const SizeT shadowBytes = + static_cast(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]); + const Bool keysMatch = memo.valid && memo.glContextId == MG_State::pGLContext->GetTextureContextId() && + memo.maxTouchedUnit == maxTouchedUnit && + memo.bindGeneration == MG_State::pGLContext->GetTextureBindGeneration() && + memo.samplingResolutionGeneration == + MG_State::pGLContext->GetSamplingResolutionGeneration() && + memo.program == static_cast(currentProgram.get()) && + memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) && + memo.programBackendStateVersion == + (currentProgram ? currentProgram->GetBackendStateVersion() : 0) && + memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) && + memo.contextGeneration == TextureImpl::g_textureContextGeneration; + // Short-circuited: the shadow compare is only meaningful once the key (and with it the + // snapshotted row count) matches. + if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), + shadowBytes) != 0) { + memo.valid = false; + if (ResolveAndBindUnitTextures(currentProgram, maxTouchedUnit)) { + memo.glContextId = MG_State::pGLContext->GetTextureContextId(); + memo.maxTouchedUnit = maxTouchedUnit; + memo.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration(); + memo.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration(); + memo.program = currentProgram.get(); + memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0; + memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0; + memo.programLinked = currentProgram && currentProgram->GetLinkStatus(); + memo.contextGeneration = TextureImpl::g_textureContextGeneration; + std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes); + memo.valid = true; + } + } + + BindCurrentUnitSamplers(maxTouchedUnit); + } + // Binds the current program's backend object and re-establishes its per-program // resources: global UBO contents, uniform-block bindings, and sampler uniform // units (layout(binding=N) qualifiers are stripped from transpiled ESSL, so the diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index d0baf621..ce6643c4 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -120,6 +120,18 @@ namespace MobileGL { // per-draw sampled-texture set. Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); } void BumpTextureBindGeneration() { m_textureState.BumpTextureBindGeneration(); } + // Monotonic counter bumped whenever a texture's shape or a sampler object's + // parameters change, i.e. whenever a bound texture's mipmap-completeness (and so + // whether a backend binds it at all) can have flipped without any bind moving; + // see TextureState::GetSamplingResolutionGeneration. + Uint64 GetSamplingResolutionGeneration() const { + return m_textureState.GetSamplingResolutionGeneration(); + } + void BumpSamplingResolutionGeneration() { m_textureState.BumpSamplingResolutionGeneration(); } + // Never-reused id of this context, for backend memos keyed on the two counters + // above: both restart at 0 in a new context, and a recreated context can land on + // the old heap address. See TextureState::GetContextId. + Uint64 GetTextureContextId() const { return m_textureState.GetContextId(); } Bool ValidateTextureName(Uint index) const; Bool ValidateTextureObject(Uint index) const; Int GetActiveTextureUnit() const; diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp index 97bd63cd..f2470f65 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp @@ -8,6 +8,8 @@ #include "SamplerObject.h" +#include + #include namespace MobileGL { @@ -22,81 +24,91 @@ namespace MobileGL { SamplerObject::SamplerObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {} + void SamplerObject::BumpVersion() { + ++m_version; + // Every setter early-outs on an unchanged value, so this only runs on a real + // parameter change. The generation is bumped for ALL parameters, not just filter + // ones that feed mipmap-completeness: a backend memo of the resolved per-unit + // bindings must never miss an invalidation, and over-invalidating on a wrap-mode + // write costs one re-resolve. + if (pGLContext) pGLContext->BumpSamplingResolutionGeneration(); + } + void SamplerObject::SetWrapS(SamplerWrapMode mode) { if (mode == m_samplerParameters.wrapS) return; m_samplerParameters.wrapS = mode; - ++m_version; + BumpVersion(); } void SamplerObject::SetWrapT(SamplerWrapMode mode) { if (mode == m_samplerParameters.wrapT) return; m_samplerParameters.wrapT = mode; - ++m_version; + BumpVersion(); } void SamplerObject::SetWrapR(SamplerWrapMode mode) { if (mode == m_samplerParameters.wrapR) return; m_samplerParameters.wrapR = mode; - ++m_version; + BumpVersion(); } void SamplerObject::SetMinFilter(SamplerFilterMode mode) { if (mode == m_samplerParameters.minFilter) return; m_samplerParameters.minFilter = mode; - ++m_version; + BumpVersion(); } void SamplerObject::SetMagFilter(SamplerFilterMode mode) { if (mode == m_samplerParameters.magFilter) return; m_samplerParameters.magFilter = mode; - ++m_version; + BumpVersion(); } void SamplerObject::SetMipmapMode(SamplerMipmapMode mode) { if (mode == m_samplerParameters.mipmapMode) return; m_samplerParameters.mipmapMode = mode; - ++m_version; + BumpVersion(); } void SamplerObject::SetLodRange(Float minLod, Float maxLod) { if (minLod == m_samplerParameters.minLod && maxLod == m_samplerParameters.maxLod) return; m_samplerParameters.minLod = minLod; m_samplerParameters.maxLod = maxLod; - ++m_version; + BumpVersion(); } void SamplerObject::SetLodBias(Float bias) { if (bias == m_samplerParameters.lodBias) return; m_samplerParameters.lodBias = bias; - ++m_version; + BumpVersion(); } void SamplerObject::SetMaxAnisotropy(Float maxAnisotropy) { if (maxAnisotropy == m_samplerParameters.maxAnisotropy) return; m_samplerParameters.maxAnisotropy = maxAnisotropy; - ++m_version; + BumpVersion(); } void SamplerObject::SetSamplerCompareFunc(SamplerCompareFunc func) { if (func == m_samplerParameters.compareFunc) return; m_samplerParameters.compareFunc = func; - ++m_version; + BumpVersion(); } void SamplerObject::SetCompareMode(SamplerCompareMode mode) { if (mode == m_samplerParameters.compareMode) return; m_samplerParameters.compareMode = mode; - ++m_version; + BumpVersion(); } SamplerWrapMode SamplerObject::GetWrapS() const { @@ -153,7 +165,7 @@ namespace MobileGL { m_samplerParameters.borderColorUI = UintVec4(static_cast(color.x()), static_cast(color.y()), static_cast(color.z()), static_cast(color.w())); - ++m_version; + BumpVersion(); } void SamplerObject::SetBorderColorI(const IntVec4& color) { @@ -166,7 +178,7 @@ namespace MobileGL { m_samplerParameters.borderColor = FloatVec4(static_cast(color.x()), static_cast(color.y()), static_cast(color.z()), static_cast(color.w())); - ++m_version; + BumpVersion(); } void SamplerObject::SetBorderColorUI(const UintVec4& color) { @@ -179,7 +191,7 @@ namespace MobileGL { m_samplerParameters.borderColor = FloatVec4(static_cast(color.x()), static_cast(color.y()), static_cast(color.z()), static_cast(color.w())); - ++m_version; + BumpVersion(); } const FloatVec4& SamplerObject::GetBorderColor() const { diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h index 939ed788..1538dda2 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h @@ -128,6 +128,12 @@ namespace MobileGL { private: static Uint64 AllocateLifetimeId(); + // The ONLY way m_version may move. Besides marking this object's parameters + // dirty for the backends it bumps the context-wide sampling-resolution + // generation: MIN_FILTER decides whether a lookup reads the mip chain, which + // decides whether a bound texture is mipmap-complete, which decides whether a + // backend binds it on its unit at all. + void BumpVersion(); const Uint m_externalIndex; const Uint64 m_lifetimeId; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index d5791390..c82c11de 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -25,6 +25,18 @@ namespace MobileGL { return s_nextTextureLifetimeId.fetch_add(1, std::memory_order_relaxed); } + void TextureObjectBase::BumpShapeVersion() { + ++m_shapeVersion; + // Shape is what mipmap-completeness is computed from, and completeness decides + // whether a backend binds this texture on its unit at all. Nothing else tells a + // backend memo of the resolved per-unit bindings that the answer moved - no bind + // changed and the texel content may be untouched. Proxy textures (used only to + // answer PROXY queries) are never bound, so their shape churn costs a memo + // invalidation for nothing; that is accepted rather than filtered, because a + // missed bump renders wrong pixels while a spare bump only costs one re-resolve. + if (pGLContext) pGLContext->BumpSamplingResolutionGeneration(); + } + TextureObjectBase::TextureObjectBase(TextureTarget target, Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()), m_target(target) { m_sampler = MakeShared(0); @@ -85,7 +97,7 @@ namespace MobileGL { } m_internalFormat = format; - ++m_shapeVersion; + BumpShapeVersion(); ++m_textureParamsVersion; } @@ -197,7 +209,7 @@ namespace MobileGL { m_levelRange.y() = m_levelRange.x(); } ++m_textureParamsVersion; - ++m_shapeVersion; + BumpShapeVersion(); } void TextureObjectBase::SetMaxLevel(Uint maxLevel) { @@ -208,7 +220,7 @@ namespace MobileGL { m_levelRange.y() = maxLevel; ++m_textureParamsVersion; - ++m_shapeVersion; + BumpShapeVersion(); } Bool TextureObjectBase::IsImmutable() const { @@ -291,12 +303,12 @@ namespace MobileGL { void TextureObjectWithOneMipmap::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) { - ++m_shapeVersion; + BumpShapeVersion(); m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input); } void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) { - ++m_shapeVersion; + BumpShapeVersion(); m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount); } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 848a0a36..c25ceab1 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -121,6 +121,12 @@ namespace MobileGL::MG_State::GLState { protected: static Uint64 AllocateLifetimeId(); + // The ONLY way m_shapeVersion may move. Besides invalidating this object's own + // completeness memo it bumps the context-wide sampling-resolution generation, which is + // what a backend memo of the resolved per-unit bindings watches: completeness decides + // whether a bound texture reaches its native target at all, and a shape change is + // otherwise invisible to such a memo (no bind moved). + void BumpShapeVersion(); const Uint m_externalIndex; const Uint64 m_lifetimeId; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index 7732a9b5..2c8929c1 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -28,12 +28,12 @@ namespace MobileGL { void TextureObject2DCube::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) { - ++m_shapeVersion; + BumpShapeVersion(); m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input); } void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) { - ++m_shapeVersion; + BumpShapeVersion(); m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount); } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index 876c3134..f65269c1 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -7,6 +7,8 @@ // End of Source File Header #include "TextureState.h" + +#include #include "Defines.h" #include "TextureEnum.h" #include "TextureObject.h" @@ -18,6 +20,12 @@ #include "TextureObjectStubs.h" namespace MobileGL::MG_State::GLState { + static std::atomic s_nextTextureStateContextId = 1; + + Uint64 TextureState::AllocateContextId() { + return s_nextTextureStateContextId.fetch_add(1, std::memory_order_relaxed); + } + static SharedPtr MakeTextureObjectForTarget(Uint index, TextureTarget target) { switch (target) { case TextureTarget::Texture1D: @@ -50,7 +58,7 @@ namespace MobileGL::MG_State::GLState { } } - TextureState::TextureState() : m_indexGenerator(1024, 1) { + TextureState::TextureState() : m_contextId(AllocateContextId()), m_indexGenerator(1024, 1) { // GL 3.3 core 3.8: each target owns one default texture object (name 0) per context, // shared across all texture units, and it is the initial binding of every unit/target // slot. It is created outside m_textureObjects so name-based paths (glIsTexture, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index 0a7c16b3..c2d1c9f8 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -84,8 +84,33 @@ namespace MobileGL::MG_State::GLState { Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; } void BumpTextureBindGeneration() { ++m_textureBindGeneration; } + // Sibling of the bind generation for everything that changes WHICH native texture a + // backend ends up putting on a unit WITHOUT any binding moving. Two families feed it: + // a texture's SHAPE (internal format, stored level set, level range - all that + // mipmap-completeness is computed from) and any sampler object's parameters (MIN_FILTER + // decides whether the mip chain is read at all, and an incomplete-for-the-filter texture + // is deliberately left unbound so it samples as (0,0,0,1)). Deliberately coarse - ANY + // texture, ANY sampler - so that no mutation can slip past a per-unit binding memo; the + // setters that feed it all early-out when the value is unchanged, so the redundant + // glTexParameteri calls applications issue every frame do not churn it. Kept separate + // from the bind generation because the sampled texture SET is unaffected by these, and + // the Vulkan backend's set memo keys on that one. + Uint64 GetSamplingResolutionGeneration() const { return m_samplingResolutionGeneration; } + void BumpSamplingResolutionGeneration() { ++m_samplingResolutionGeneration; } + + // Globally-unique, never-reused id of THIS texture state, i.e. of the context that owns + // it. Both generations above restart at 0 with a new context, so a backend memo keyed on + // them alone would accept a destroyed-and-recreated context whose counters happen to line + // up - and the heap address is no help, since a context freed and remade lands on it + // again (the unit tests do exactly that between cases). + Uint64 GetContextId() const { return m_contextId; } + private: + static Uint64 AllocateContextId(); + + const Uint64 m_contextId; Uint64 m_textureBindGeneration = 0; + Uint64 m_samplingResolutionGeneration = 0; Int m_maxTouchedUnit = -1; Int m_activeTextureUnit = 0; Array m_textureUnits; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp b/MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp index b65e1900..8946c48e 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp @@ -8,6 +8,8 @@ #include "TextureUnit.h" +#include + namespace MobileGL::MG_State::GLState { TextureUnit::TextureUnit() : m_sampler(nullptr) { for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) { @@ -24,7 +26,17 @@ namespace MobileGL::MG_State::GLState { } void TextureUnit::SetSamplerObject(const SharedPtr& sampler) { + if (m_sampler == sampler) return; + m_sampler = sampler; + // Which sampler object a unit carries is part of "what is bound at this unit": it + // overrides the texture's own sampler state, so it selects the filter that decides a + // bound texture's mipmap-completeness. glBindSampler already bumps the generation + // through NoteUnitTouched, but glDeleteSamplers unbinds the deleted object from every + // unit straight through here (GLContext::MarkSamplerObjectForDeletion) and would + // otherwise leave a backend memo of the resolved per-unit bindings replaying the + // deleted sampler. + if (pGLContext) pGLContext->BumpTextureBindGeneration(); } const SharedPtr& TextureUnit::GetSamplerObject() const {