From 76b8957b997b736d4b859cadd824c369599fde82 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 30 Jul 2026 07:02:02 -0400 Subject: [PATCH] [Perf] (DirectVulkan): memoize sampled-texture resources across draws --- .../Renderer/VkTextureManager.cpp | 94 ++++++++++++------- .../DirectVulkan/Renderer/VkTextureManager.h | 17 ++++ 2 files changed, 75 insertions(+), 36 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 3d06fd44..14abdcab 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -611,6 +611,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { ReclaimCompletedUploads(/*waitAll=*/true); } DestroyDeferredReleases(); + ++m_resourceEraseEpoch; // every memoized resource pointer dies with the map m_textureResources.clear(); m_aliveObjects.clear(); m_storageImageTextures.clear(); @@ -663,6 +664,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { } m_aliveObjects.erase(identity); m_storageImageTextures.erase(identity); + // Invalidate every cross-draw sampled-texture memo: the erased + // resource's address may be reused by a future emplace. + ++m_resourceEraseEpoch; } void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) { @@ -718,45 +722,63 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - auto aliveIt = m_aliveObjects.find(identity); - if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) { - EraseTrackedTexture(aliveIt->first); - aliveIt = m_aliveObjects.end(); - } - - // Only (re)register and prune when this (texture, lifetime) pair is new: stale - // aliases can only come into existence through an address reuse, which by - // construction introduces a new identity. Doing this unconditionally made every - // sampled-texture sync scan the entire alive-texture map per draw. - if (aliveIt == m_aliveObjects.end()) { - WeakPtr aliveTexture; - const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex()); - if (liveTexture && liveTexture.get() == &texture) { - aliveTexture = liveTexture; - } else { - // The name lookup legally fails while the object is alive: the name was - // deleted with the texture still attached to an FBO (the attachment's - // SharedPtr keeps it alive), or the name was reused by a new texture, or - // this is a default texture object (name 0 lives outside the name map). - // Register through the object's own control block so the resource created - // below still participates in weak-expiry GC instead of becoming an - // orphan no reclamation path can reach until Shutdown. - aliveTexture = texture.weak_from_this(); - } - if (!aliveTexture.expired()) { - m_aliveObjects[identity] = Move(aliveTexture); - PruneStaleTextureAliases(&texture); + // Cross-draw memo probe (see SyncedTextureMemoEntry): skips both map + // lookups and the (re)registration path for repeat-bound textures. + TextureResource* resourcePtr = nullptr; + for (Uint32 i = 0; i < kSyncedTextureMemoSize; ++i) { + const SyncedTextureMemoEntry& memo = m_syncedTextureMemo[i]; + if (memo.texture == &texture && memo.lifetimeId == identity.lifetimeId && + memo.eraseEpoch == m_resourceEraseEpoch) { + resourcePtr = memo.resource; + break; } } - auto it = m_textureResources.find(identity); - if (it == m_textureResources.end()) { - TextureResource initial{}; - auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial)); - it = insertIt; + if (resourcePtr == nullptr) { + auto aliveIt = m_aliveObjects.find(identity); + if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) { + EraseTrackedTexture(aliveIt->first); + aliveIt = m_aliveObjects.end(); + } + + // Only (re)register and prune when this (texture, lifetime) pair is new: stale + // aliases can only come into existence through an address reuse, which by + // construction introduces a new identity. Doing this unconditionally made every + // sampled-texture sync scan the entire alive-texture map per draw. + if (aliveIt == m_aliveObjects.end()) { + WeakPtr aliveTexture; + const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex()); + if (liveTexture && liveTexture.get() == &texture) { + aliveTexture = liveTexture; + } else { + // The name lookup legally fails while the object is alive: the name was + // deleted with the texture still attached to an FBO (the attachment's + // SharedPtr keeps it alive), or the name was reused by a new texture, or + // this is a default texture object (name 0 lives outside the name map). + // Register through the object's own control block so the resource created + // below still participates in weak-expiry GC instead of becoming an + // orphan no reclamation path can reach until Shutdown. + aliveTexture = texture.weak_from_this(); + } + if (!aliveTexture.expired()) { + m_aliveObjects[identity] = Move(aliveTexture); + PruneStaleTextureAliases(&texture); + } + } + + auto it = m_textureResources.find(identity); + if (it == m_textureResources.end()) { + TextureResource initial{}; + auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial)); + it = insertIt; + } + resourcePtr = &(it->second); + m_syncedTextureMemo[m_syncedTextureMemoNext] = + SyncedTextureMemoEntry{&texture, identity.lifetimeId, m_resourceEraseEpoch, resourcePtr}; + m_syncedTextureMemoNext = (m_syncedTextureMemoNext + 1) % kSyncedTextureMemoSize; } - if (!SyncTexture(texture, it->second)) { + if (!SyncTexture(texture, *resourcePtr)) { MGLOG_D("%s: Syncing texture %d failed", __func__, texture.GetExternalIndex()); return nullptr; } @@ -770,11 +792,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } if (!recorded) { - m_drawSyncedThisDraw.push_back({identity, &(it->second)}); + m_drawSyncedThisDraw.push_back({identity, resourcePtr}); } } - return &(it->second); + return resourcePtr; } VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 54fa5e1f..33b683d8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -454,6 +454,23 @@ private: TextureResource* resource = nullptr; }; Vector m_drawSyncedThisDraw; + // Cross-draw sampled-texture memo: the same few textures (atlas, lightmap) + // are resolved on every draw, so cache their resource pointers and skip the + // alive/resource map lookups. Node-based std::unordered_map keeps the + // pointees stable across inserts; erases bump m_resourceEraseEpoch, which + // every memo entry must match. SyncTexture still runs on memo hits, so + // content/param freshness is unaffected. A dead-then-reused texture address + // cannot false-hit: the new object carries a new lifetime id. + struct SyncedTextureMemoEntry { + const MG_State::GLState::ITextureObject* texture = nullptr; + Uint64 lifetimeId = 0; + Uint64 eraseEpoch = 0; + TextureResource* resource = nullptr; + }; + static constexpr Uint32 kSyncedTextureMemoSize = 8; + SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize]; + Uint32 m_syncedTextureMemoNext = 0; + Uint64 m_resourceEraseEpoch = 1; // Formats whose mutable-image probe failed on this device; their images are created // without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch. std::unordered_set m_mutableFormatUnsupported;