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 {