diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 13406068..9b308e27 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -107,6 +107,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_minDynamicOffsetAlignment = std::max(1, minUniformBufferOffsetAlignment); m_frameCount = frameCount; m_maxBindings = maxBindings; + m_samplerResolveMemo.assign(m_maxBindings, SamplerResolveMemo{}); m_setsPerFrame = setsPerFrame; m_peakDescriptorSetsObserved = 0; m_textureManager = textureManager; @@ -196,6 +197,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The frame's descriptor sets are recycled above, so last frame's reuse target // is gone: start the per-draw descriptor-reuse cache fresh this frame. m_hasLastDescriptor = false; + // Re-fingerprint the bound sampler set fresh this frame so any GL object address + // reuse cannot outlive a single frame (see SamplerResolveMemo). + for (auto& memo : m_samplerResolveMemo) { + memo.valid = false; + } } Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, @@ -274,8 +280,37 @@ namespace MobileGL::MG_Backend::DirectVulkan { "ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u", static_cast(resource->layout), texture->GetExternalIndex(), binding); } + // Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's + // sampler object and texture (both by lifetime id + version) are unchanged from the + // last draw that resolved it: the resulting sampler key, and therefore the VkSampler + // handle, are guaranteed identical. Lifetime ids are never reused, so a freed-and- + // reallocated sampler/texture at the same address gets a fresh id and misses instead + // of false-hitting. Cached handles live until Shutdown, so the memo can never hand + // back a destroyed sampler. + VkSampler resolvedSampler = VK_NULL_HANDLE; + if (binding < m_samplerResolveMemo.size()) { + auto& memo = m_samplerResolveMemo[binding]; + const Uint64 samplerLifetimeId = samplerToUse->GetLifetimeId(); + const Uint16 samplerVersion = samplerToUse->GetVersion(); + const Uint64 textureLifetimeId = texture->GetLifetimeId(); + const Uint16 textureParamsVersion = texture->GetTextureParamsVersion(); + if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion && + memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion) { + resolvedSampler = memo.sampler; + } else { + resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture); + memo.samplerLifetimeId = samplerLifetimeId; + memo.samplerVersion = samplerVersion; + memo.textureLifetimeId = textureLifetimeId; + memo.textureParamsVersion = textureParamsVersion; + memo.sampler = resolvedSampler; + memo.valid = true; + } + } else { + resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture); + } outImageInfo = { - .sampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture), + .sampler = resolvedSampler, .imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView, .imageLayout = resource->layout, }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 00ffc4d3..0879e7e8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -146,6 +146,28 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE; Uint64 m_lastDescriptorSignature = 0; Bool m_hasLastDescriptor = false; + + // Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which + // stays the source of truth: its key hashes all sampler+texture state, so two distinct + // sampler objects with identical state still resolve to one VkSampler. This memo only + // skips recomputing that hash. Across a draw batch the bound sampler set is stable, so a + // binding whose sampler (lifetime id + version, bumped on every setter) and texture + // (lifetime id + params version, bumped on the format/border-color setters that feed the + // key) are unchanged recycles the VkSampler it resolved last draw; a param change bumps + // a version and forces a re-resolve. Both objects are keyed by a never-reused monotonic + // lifetime id, so a freed-and-reallocated sampler or texture at the same heap address + // always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a + // stale guess can only miss and fall through to the hash, never resolve wrong. Still + // reset each frame alongside the descriptor-set cache. Indexed by binding. + struct SamplerResolveMemo { + Uint64 samplerLifetimeId = 0; + Uint64 textureLifetimeId = 0; + VkSampler sampler = VK_NULL_HANDLE; + Uint16 samplerVersion = 0; + Uint16 textureParamsVersion = 0; + Bool valid = false; + }; + mutable Vector m_samplerResolveMemo; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp index e5c1377a..68569872 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp @@ -8,10 +8,19 @@ #include "SamplerObject.h" +#include + namespace MobileGL { namespace MG_State { namespace GLState { - SamplerObject::SamplerObject(Uint externalIndex) : m_externalIndex(externalIndex) {} + static std::atomic s_nextSamplerLifetimeId = 1; + + Uint64 SamplerObject::AllocateLifetimeId() { + return s_nextSamplerLifetimeId.fetch_add(1, std::memory_order_relaxed); + } + + SamplerObject::SamplerObject(Uint externalIndex) + : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {} void SamplerObject::SetWrapS(SamplerWrapMode mode) { if (mode == m_samplerParameters.wrapS) return; @@ -138,6 +147,10 @@ namespace MobileGL { Uint16 SamplerObject::GetVersion() const { return m_version; } + + Uint64 SamplerObject::GetLifetimeId() const { + return m_lifetimeId; + } } // namespace GLState } // namespace MG_State } // namespace MobileGL diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h index c1468677..16fda9b0 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h @@ -99,10 +99,18 @@ namespace MobileGL { SamplerCompareFunc GetSamplerCompareFunc() const; Uint GetExternalIndex() const; Uint16 GetVersion() const; + // Globally-unique, never-reused id for this sampler object's lifetime. Lets a + // cache distinguish a freed-and-reallocated sampler (same heap address, GL name, + // or version count) from the original - the sampler analogue of the texture + // lifetime id. Used by the Vulkan backend's per-binding sampler fast path. + Uint64 GetLifetimeId() const; const SamplerParameters& GetAllSamplerParameters() const; private: + static Uint64 AllocateLifetimeId(); + const Uint m_externalIndex; + const Uint64 m_lifetimeId; Uint16 m_version = 0; SamplerParameters m_samplerParameters; };