[Fix] (DirectVulkan): harden the leak-fix round after adversarial review - pipeline memo now drops at every command-buffer boundary (a flush-loop-memoized pipeline could age out and be destroyed while its submission was in flight), mid-frame drains no longer rewind the arena or advance the cache-aging clocks in presenting apps (gated to every 8th drain since the last Present, so readback/fence-heavy frames neither churn conversions nor shrink the 1024-boundary retire window), render-pass eviction notifies the pipeline cache once per sweep batch instead of once per dying pass, descriptor pools use FREE_DESCRIPTOR_SET_BIT so a destroyed layout's cached sets are freed back and credited instead of abandoning pool slots (the live-layout age sweep that could orphan slots is removed - layout destruction is the sole purge path), and renderbuffer respecify parks the old backing for aged destruction instead of destroying it while possibly in flight

This commit is contained in:
2026-07-27 22:51:26 -04:00
parent d076c29146
commit 8a0a8a0274
8 changed files with 207 additions and 117 deletions
@@ -41,16 +41,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BeginFrame(Uint32 frameIndex);
// A ProgramFactory eviction just destroyed this layout: purge every frame
// slot's cached descriptor sets for it, so a recycled handle value can never
// stale-hit sets written for the dead layout's bindings. The sets themselves
// are only dropped, never freed - the frame pools are created without
// FREE_DESCRIPTOR_SET_BIT and are never reset mid-life, so their pool slots
// stay occupied until Shutdown destroys the pools wholesale.
// stale-hit sets written for the dead layout's bindings. The sets are
// vkFreeDescriptorSets'd back to their pools (created with
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
// references its sets. This is the only eviction path for the per-layout
// caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
// Frame boundary hook: ages the per-layout descriptor-set caches and drops
// long-unused entries (mirroring VkRenderPassManager::OnPresent's sweep), so a
// shader-churn session's peak layout population does not pin its tracking
// vectors forever. Same pool-slot caveat as above.
void OnFrameBoundary();
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures);
@@ -77,12 +76,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 allocatedSets = 0;
};
// A cached descriptor set together with the pool it was allocated from, so a
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
// owning bucket's accounting.
struct CachedDescriptorSet {
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
};
struct DescriptorSetCacheEntry {
Vector<VkDescriptorSet> sets;
Vector<CachedDescriptorSet> sets;
Uint32 cursor = 0;
// Frame-boundary counter value of the last AcquireDescriptorSet hit; drives
// cache eviction (see OnFrameBoundary).
Uint64 lastUsedFrame = 0;
};
struct FrameResources {
@@ -146,9 +150,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<FrameResources> m_frames;
VkDeviceSize m_minDynamicOffsetAlignment = 1;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for
// descriptor-set cache aging.
Uint64 m_frameCounter = 0;
Uint32 m_frameCount = 0;
Uint32 m_maxBindings = 0;
Uint32 m_setsPerFrame = 0;