[Perf] (MG_Backend/DirectVulkan): cache resolved VkSampler per binding to skip the per-draw sampler key hash, keyed on a new sampler lifetime id

This commit is contained in:
2026-07-13 02:04:57 -04:00
parent 808c5dcc46
commit 516d2a659e
4 changed files with 80 additions and 2 deletions
@@ -107,6 +107,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_minDynamicOffsetAlignment = std::max<VkDeviceSize>(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<Int>(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,
};
@@ -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<SamplerResolveMemo> m_samplerResolveMemo;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,10 +8,19 @@
#include "SamplerObject.h"
#include <atomic>
namespace MobileGL {
namespace MG_State {
namespace GLState {
SamplerObject::SamplerObject(Uint externalIndex) : m_externalIndex(externalIndex) {}
static std::atomic<Uint64> 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
@@ -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;
};