mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 12:18:30 +09:00
[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:
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "PipelineFactory.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||
@@ -297,10 +298,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByRenderPass(VkRenderPass renderPass) {
|
||||
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
|
||||
if (renderPasses.empty() || m_cache.empty()) {
|
||||
return 0;
|
||||
}
|
||||
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
|
||||
// dimension exit) at one O(cache * log batch) scan instead of one full scan
|
||||
// per dying pass.
|
||||
Vector<VkRenderPass> sortedPasses = renderPasses;
|
||||
std::sort(sortedPasses.begin(), sortedPasses.end());
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (it->second.renderPass == renderPass) {
|
||||
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
@@ -311,8 +320,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByRenderPass: evicted %u pipelines for destroyed render pass",
|
||||
evicted);
|
||||
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
|
||||
evicted, sortedPasses.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
@@ -70,15 +70,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
|
||||
// destroyed so the caller can drop any memoized VkPipeline handle.
|
||||
Uint32 OnFrameBoundary();
|
||||
// Destroys every cached pipeline hashed on `renderPass`. Only safe when the
|
||||
// caller guarantees GPU idleness for them - the render-pass manager calls this
|
||||
// (via the renderer) for passes its own >1024-boundary-idle sweep just evicted,
|
||||
// and a pipeline hashed on that handle is only ever bound by draws that also
|
||||
// hit the render-pass entry. Also closes the handle-recycling hazard: a
|
||||
// recycled VkRenderPass value must never serve a stale pipeline. Returns the
|
||||
// number destroyed (callers invalidate memos when non-zero); linear scan is
|
||||
// fine, evictions are rare.
|
||||
Uint32 EvictByRenderPass(VkRenderPass renderPass);
|
||||
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
|
||||
// when the caller guarantees GPU idleness for them - the render-pass manager
|
||||
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
|
||||
// just evicted, and a pipeline hashed on those handles is only ever bound by
|
||||
// draws that also hit the render-pass entries. Also closes the handle-recycling
|
||||
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
|
||||
// Batched: one cache scan regardless of how many passes died in the sweep.
|
||||
// Returns the number destroyed (callers invalidate memos when non-zero).
|
||||
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
|
||||
// Destroys every cached pipeline built from the program with content hash
|
||||
// `programHash`. Called from the ProgramFactory eviction path, which proves the
|
||||
// same >1024-boundary idleness (the program's pipelines are only bound by draws
|
||||
|
||||
@@ -171,7 +171,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_programFactory = nullptr;
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_minDynamicOffsetAlignment = 1;
|
||||
m_frameCounter = 0;
|
||||
m_frameCount = 0;
|
||||
m_maxBindings = 0;
|
||||
m_setsPerFrame = 0;
|
||||
@@ -216,49 +215,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SizeT purgedSets = 0;
|
||||
for (auto& frame : m_frames) {
|
||||
const auto it = frame.descriptorSetCacheByLayout.find(descriptorSetLayout);
|
||||
if (it != frame.descriptorSetCacheByLayout.end()) {
|
||||
purgedSets += it->second.sets.size();
|
||||
frame.descriptorSetCacheByLayout.erase(it);
|
||||
if (it == frame.descriptorSetCacheByLayout.end()) {
|
||||
continue;
|
||||
}
|
||||
// Free the sets back to their pools and credit the bucket accounting, so
|
||||
// program churn recycles pool capacity instead of abandoning the slots.
|
||||
// GPU-safe: the layout only dies after >1024 idle frame boundaries, so no
|
||||
// in-flight command buffer references these sets.
|
||||
for (const auto& cached : it->second.sets) {
|
||||
if (cached.set == VK_NULL_HANDLE) {
|
||||
continue;
|
||||
}
|
||||
vkFreeDescriptorSets(m_device, cached.pool, 1, &cached.set);
|
||||
const auto bucket = std::find_if(
|
||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||
[&cached](const DescriptorPoolBucket& candidate) { return candidate.handle == cached.pool; });
|
||||
if (bucket != frame.descriptorPools.end() && bucket->allocatedSets > 0) {
|
||||
--bucket->allocatedSets;
|
||||
}
|
||||
}
|
||||
purgedSets += it->second.sets.size();
|
||||
frame.descriptorSetCacheByLayout.erase(it);
|
||||
}
|
||||
if (purgedSets > 0) {
|
||||
// The per-draw reuse memo folds the layout handle into its signature; drop
|
||||
// it so a recycled handle value cannot revive a purged set mid-frame.
|
||||
m_hasLastDescriptor = false;
|
||||
MGLOG_D("UniformDescriptorBinder: purged %zu descriptor sets for destroyed layout", purgedSets);
|
||||
}
|
||||
}
|
||||
|
||||
void UniformManager::OnFrameBoundary() {
|
||||
++m_frameCounter;
|
||||
|
||||
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent. Only the
|
||||
// CPU-side tracking is reclaimed here: the sets' pool slots stay occupied until
|
||||
// Shutdown destroys the pools (no FREE_DESCRIPTOR_SET_BIT, no mid-life pool
|
||||
// reset), exactly as they would had the entry been kept. What the sweep buys is
|
||||
// that an idle layout's tracking vectors stop accumulating, and that a
|
||||
// destroyed-then-recycled layout handle finds no stale entry to hit.
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
SizeT purgedSets = 0;
|
||||
for (auto& frame : m_frames) {
|
||||
for (auto it = frame.descriptorSetCacheByLayout.begin();
|
||||
it != frame.descriptorSetCacheByLayout.end();) {
|
||||
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
purgedSets += it->second.sets.size();
|
||||
it = frame.descriptorSetCacheByLayout.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (purgedSets > 0) {
|
||||
m_hasLastDescriptor = false;
|
||||
MGLOG_D("UniformDescriptorBinder::OnFrameBoundary: dropped %zu idle descriptor sets", purgedSets);
|
||||
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,6 +945,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
// FREE_DESCRIPTOR_SET_BIT lets a destroyed layout's cached sets be freed back
|
||||
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
|
||||
// The cost is on set allocation only, which happens when a layout's per-frame
|
||||
// cache grows - never on the per-draw reuse path.
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.maxSets = maxSets;
|
||||
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
||||
poolInfo.pPoolSizes = poolSizes;
|
||||
@@ -1040,12 +1028,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorSet& outDescriptorSet) {
|
||||
auto& frame = m_frames[frameIndex];
|
||||
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
|
||||
// Every layout used in a frame is acquired at least once (the per-draw
|
||||
// descriptor-reuse memo starts each frame invalidated and folds the layout
|
||||
// into its signature), so an actively-used entry is stamped every frame.
|
||||
cache.lastUsedFrame = m_frameCounter;
|
||||
if (cache.cursor < cache.sets.size()) {
|
||||
outDescriptorSet = cache.sets[cache.cursor++];
|
||||
outDescriptorSet = cache.sets[cache.cursor++].set;
|
||||
} else {
|
||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||
@@ -1059,7 +1043,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return allocResult;
|
||||
}
|
||||
|
||||
cache.sets.push_back(outDescriptorSet);
|
||||
// The successful allocation came from the bucket the alloc helper left
|
||||
// active; record it so a layout-destroyed purge can free the set back.
|
||||
cache.sets.push_back({outDescriptorSet, frame.descriptorPools[frame.activeDescriptorPoolIndex].handle});
|
||||
++cache.cursor;
|
||||
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
|
||||
cache.sets.size());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -207,6 +207,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.Destroy(m_device, m_allocator);
|
||||
}
|
||||
m_renderbufferResources.clear();
|
||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
|
||||
m_pendingRenderbufferClears.clear();
|
||||
RenderPassEntry::s_textureResourcesScratch.clear();
|
||||
s_activeRenderPass = {};
|
||||
@@ -214,19 +215,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
|
||||
Uint64 VkRenderPassManager::RetireAgeFrames() const {
|
||||
// MaxFramesInFlight + 2 covers the frame ring plus one boundary for the
|
||||
// recording-to-submit gap and one because OnPresent runs ahead of Present's
|
||||
// fence wait; the floor of 8 keeps a margin over the default ring of 3 while
|
||||
// still releasing multi-MB attachment memory promptly (the render-pass cache's
|
||||
// 1024-frame retirement would pin it for no additional safety).
|
||||
return std::max<Uint64>(8, static_cast<Uint64>(m_config.MaxFramesInFlight) + 2);
|
||||
}
|
||||
|
||||
void VkRenderPassManager::DeferRenderbufferBackingRelease(RenderbufferResource& resource) {
|
||||
// The superseded backing may still be referenced by in-flight command buffers
|
||||
// (glRenderbufferStorage can respecify a renderbuffer drawn this very frame),
|
||||
// so it is parked and destroyed only after RetireAgeFrames() boundaries.
|
||||
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
m_deferredRenderbufferReleases.push_back({resource.image, resource.allocation, resource.view, m_frameCounter});
|
||||
resource.image = VK_NULL_HANDLE;
|
||||
resource.allocation = nullptr;
|
||||
resource.view = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
|
||||
if (m_deferredRenderbufferReleases.empty()) {
|
||||
return;
|
||||
}
|
||||
const Uint64 retireAgeFrames = RetireAgeFrames();
|
||||
std::erase_if(m_deferredRenderbufferReleases, [&](DeferredRenderbufferRelease& release) {
|
||||
if (!destroyAll && m_frameCounter - release.deferredAtFrame < retireAgeFrames) {
|
||||
return false;
|
||||
}
|
||||
if (release.view != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(m_device, release.view, nullptr);
|
||||
}
|
||||
if (release.image != VK_NULL_HANDLE) {
|
||||
vmaDestroyImage(m_allocator, release.image, release.allocation);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void VkRenderPassManager::CollectRenderbufferGarbage() {
|
||||
// Two-phase reclamation: a dead renderbuffer's VkImage may still be referenced by
|
||||
// command buffers submitted up to frames-in-flight frames ago (it was legally
|
||||
// attached and drawn right up to its deletion), so the first observation of an
|
||||
// expired weak reference only stamps the current frame counter; Destroy runs once
|
||||
// enough frame boundaries have passed that the stamping frame's submission fence
|
||||
// has provably been waited. MaxFramesInFlight + 2 covers the frame ring plus one
|
||||
// boundary for the recording-to-submit gap and one because OnPresent runs ahead
|
||||
// of Present's fence wait; the floor of 8 keeps a margin over the default ring of
|
||||
// 3 while still releasing multi-MB attachment memory promptly (the render-pass
|
||||
// cache's 1024-frame retirement would pin it for no additional safety).
|
||||
const Uint64 retireAgeFrames =
|
||||
std::max<Uint64>(8, static_cast<Uint64>(m_config.MaxFramesInFlight) + 2);
|
||||
// has provably been waited (see RetireAgeFrames).
|
||||
const Uint64 retireAgeFrames = RetireAgeFrames();
|
||||
for (auto it = m_renderbufferResources.begin(); it != m_renderbufferResources.end();) {
|
||||
auto& resource = it->second;
|
||||
const auto liveRenderbuffer = resource.renderbuffer.lock();
|
||||
@@ -294,6 +331,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return &resource;
|
||||
}
|
||||
|
||||
// Respecify: park the old backing for aged destruction instead of destroying
|
||||
// inline - it may still be referenced by in-flight command buffers.
|
||||
DeferRenderbufferBackingRelease(resource);
|
||||
resource.Destroy(m_device, m_allocator);
|
||||
resource.renderbuffer = renderbuffer;
|
||||
|
||||
@@ -1205,6 +1245,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// coupling it to renderbuffer *use* (the GetOrCreateRenderbufferResource call
|
||||
// site never runs again once an app stops using renderbuffers).
|
||||
CollectRenderbufferGarbage();
|
||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/false);
|
||||
|
||||
// Sweep occasionally; evict entries whose last use is far past every
|
||||
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
||||
@@ -1215,6 +1256,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the dying handles and notify once after the loop: pipelines hashed
|
||||
// on them share the entries' >kRetireAgeFrames idleness (they are only bound
|
||||
// by draws that hit those entries), so the observer may destroy them
|
||||
// immediately - and a single batched notification costs one pipeline-cache
|
||||
// scan instead of one per evicted pass.
|
||||
Vector<VkRenderPass> destroyedRenderPasses;
|
||||
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
||||
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
||||
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
||||
@@ -1222,17 +1269,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
// Notify while the handle is still alive: pipelines hashed on it share
|
||||
// the entry's >kRetireAgeFrames idleness (they are only bound by draws
|
||||
// that hit this entry), so the observer may destroy them immediately.
|
||||
if (m_evictionObserver != nullptr) {
|
||||
m_evictionObserver->OnRenderPassDestroyed(it->second.renderPass);
|
||||
}
|
||||
destroyedRenderPasses.push_back(it->second.renderPass);
|
||||
it = m_renderPasses.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
|
||||
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
||||
|
||||
@@ -158,16 +158,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
|
||||
// Notified from the OnPresent sweep for each aged-out entry, BEFORE the entry
|
||||
// destructor destroys its VkRenderPass: pipelines are hashed on the raw handle,
|
||||
// and once destroyed the value may be recycled for an incompatible pass, so
|
||||
// dependent caches must purge everything keyed on it in the same step. The
|
||||
// wholesale paths (Shutdown/RecreateSwapchain) do not notify - their callers
|
||||
// already drop every pipeline outright.
|
||||
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
|
||||
// value: pipelines are hashed on the raw handle, and once destroyed the value
|
||||
// may be recycled for an incompatible pass, so dependent caches must purge
|
||||
// everything keyed on them before any new pass can be created (the sweep and
|
||||
// the notification run back-to-back with no creation in between; observers
|
||||
// compare the values, never dereference them). Batched so a mass-idle cohort
|
||||
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
|
||||
// scan, not one per dying pass. The wholesale paths
|
||||
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
|
||||
// every pipeline outright.
|
||||
class IEvictionObserver {
|
||||
public:
|
||||
virtual ~IEvictionObserver() = default;
|
||||
virtual void OnRenderPassDestroyed(VkRenderPass renderPass) = 0;
|
||||
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
|
||||
};
|
||||
|
||||
VkRenderPassManager(VkDevice device,
|
||||
@@ -266,12 +270,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ClearAttachmentPayload payload{};
|
||||
};
|
||||
|
||||
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
|
||||
// until enough frame boundaries have passed that no in-flight command buffer
|
||||
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
|
||||
struct DeferredRenderbufferRelease {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
|
||||
Bool HasPendingRenderbufferClear(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||
void CollectRenderbufferGarbage();
|
||||
// Frame-boundary margin after which a resource last referenced by a retired
|
||||
// GL object (or superseded backing) is provably past every in-flight frame.
|
||||
Uint64 RetireAgeFrames() const;
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||
|
||||
@@ -7228,11 +7228,10 @@ void main() {
|
||||
}
|
||||
|
||||
// Every submission is complete and nothing recorded references the
|
||||
// per-frame transients, so the drains Present's tail performs are safe
|
||||
// here too. Raise the buffer manager's completed floor first: the
|
||||
// serial inference (frameSerial - frameCount) is only justified by
|
||||
// Present's slot-fence cadence, and the extra BeginFrame below would
|
||||
// otherwise inflate it past reality.
|
||||
// per-frame transients. Pure-reclaim work runs on every drain: it only
|
||||
// releases memory that is provably dead, never invalidates anything a
|
||||
// later draw would have to rebuild. Raise the buffer manager's
|
||||
// completed floor first so busy-tracking reflects the proven idleness.
|
||||
m_bufferManager.NotifyDeviceIdle();
|
||||
|
||||
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
|
||||
@@ -7242,23 +7241,38 @@ void main() {
|
||||
}
|
||||
if (m_textureManager) {
|
||||
m_textureManager->CollectAllDeferredReleases();
|
||||
m_textureManager->BeginFrame(frameIndex);
|
||||
}
|
||||
m_bufferManager.CollectAllDeferredReleases();
|
||||
// Descriptor cursors rewind on every drain (the pre-drain readback path
|
||||
// already did exactly this), keeping fence/readback loops' set usage bounded.
|
||||
if (m_uniformManager) {
|
||||
m_uniformManager->BeginFrame(frameIndex);
|
||||
}
|
||||
|
||||
// Frame-boundary-equivalent work - transient arena rewind (which invalidates
|
||||
// the conversion cache) and the cache-aging clocks - is gated to every 8th
|
||||
// drain since the last Present: a presenting app's mid-frame readbacks/waits
|
||||
// must neither force re-conversion/re-upload churn for the rest of the frame
|
||||
// nor multiply the aging rate (which would shrink the 1024-boundary retire
|
||||
// window and thrash periodically-used pipelines/programs), while present-less
|
||||
// loops still rewind the arena and age their caches every 8 iterations -
|
||||
// bounded by 8 iterations' transient usage.
|
||||
++m_drainsSinceLastPresent;
|
||||
if ((m_drainsSinceLastPresent % 8) != 0) {
|
||||
return true;
|
||||
}
|
||||
if (m_textureManager) {
|
||||
m_textureManager->BeginFrame(frameIndex);
|
||||
}
|
||||
m_bufferManager.BeginFrame(frameIndex);
|
||||
// The cached conversion slices point into the transient arena the
|
||||
// BeginFrame above just rewound; drop them together.
|
||||
m_convertedVertexStreams.clear();
|
||||
if (m_uniformManager) {
|
||||
m_uniformManager->BeginFrame(frameIndex);
|
||||
}
|
||||
if (m_renderPassManager) {
|
||||
m_renderPassManager->OnPresent();
|
||||
}
|
||||
// Present-less loops cross frame boundaries here, so the content-addressed
|
||||
// caches age on the same cadence as Present's tail. The pipeline memo can
|
||||
// survive across these boundaries (no per-frame reset on this path), so it
|
||||
// must drop whenever the sweep destroys anything.
|
||||
// The pipeline memo can survive across these boundaries (no per-frame reset
|
||||
// on this path), so it must drop whenever the sweep destroys anything.
|
||||
if (m_programFactory) {
|
||||
m_programFactory->OnFrameBoundary();
|
||||
}
|
||||
@@ -7266,9 +7280,6 @@ void main() {
|
||||
m_lastPipelineValid = false;
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
}
|
||||
if (m_uniformManager) {
|
||||
m_uniformManager->OnFrameBoundary();
|
||||
}
|
||||
if (m_vertexInputStateFactory) {
|
||||
m_vertexInputStateFactory->OnFrameBoundary();
|
||||
}
|
||||
@@ -7366,6 +7377,14 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Command-buffer boundary: the pipeline memo must not survive it, or a
|
||||
// pipeline bound only through memo hits is never re-stamped in the factory
|
||||
// cache and the aging sweep could destroy it while the flushed submission
|
||||
// still references it. Mirrors the drops at the readback and Present
|
||||
// boundaries; costs one full pipeline lookup on the next draw.
|
||||
m_lastPipelineValid = false;
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
|
||||
// The submitted command buffer may still be executing; recording must
|
||||
// restart on a fresh one. If none can be allocated, fall back to
|
||||
// draining this submission so reusing the buffer stays legal.
|
||||
@@ -7530,16 +7549,20 @@ void main() {
|
||||
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
|
||||
"Present, acquired image index out of range");
|
||||
m_renderPassManager->OnPresent();
|
||||
// A real presented frame is the canonical aging cadence; mid-frame drains
|
||||
// count against this and only age when presents stop coming.
|
||||
m_drainsSinceLastPresent = 0;
|
||||
// Age the content-addressed caches on the same frame-boundary cadence. Each
|
||||
// keeps its own internal 256-sweep gate, so the per-frame cost is one counter
|
||||
// increment and compare per cache; entries used by this frame's still-
|
||||
// unsubmitted recording were stamped this boundary and can never age out.
|
||||
// unsubmitted recording were stamped this boundary (every command-buffer
|
||||
// boundary drops the pipeline memo, so the first draw of each recording
|
||||
// performs a real, stamping lookup) and can never age out.
|
||||
m_programFactory->OnFrameBoundary();
|
||||
if (m_pipelineFactory->OnFrameBoundary() > 0) {
|
||||
m_lastPipelineValid = false; // an aged-out pipeline may still be memoized
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
}
|
||||
m_uniformManager->OnFrameBoundary();
|
||||
m_vertexInputStateFactory->OnFrameBoundary();
|
||||
m_samplerManager->OnFrameBoundary();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
@@ -8574,15 +8597,15 @@ void main() {
|
||||
m_computePipelines.clear();
|
||||
}
|
||||
|
||||
void VulkanRenderer::OnRenderPassDestroyed(VkRenderPass renderPass) {
|
||||
void VulkanRenderer::OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) {
|
||||
if (m_pipelineFactory == nullptr) {
|
||||
return;
|
||||
}
|
||||
// The render-pass sweep's >1024-boundary idle guarantee covers these pipelines
|
||||
// too (they are only bound by draws that hit the dying entry), so the factory
|
||||
// too (they are only bound by draws that hit the dying entries), so the factory
|
||||
// destroys them immediately. The memo must drop as well: it can hand out a
|
||||
// cached handle without touching the factory.
|
||||
if (m_pipelineFactory->EvictByRenderPass(renderPass) > 0) {
|
||||
if (m_pipelineFactory->EvictByRenderPasses(renderPasses) > 0) {
|
||||
m_lastPipelineValid = false;
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
@@ -134,11 +134,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// recording, before any render pass.
|
||||
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
|
||||
|
||||
// VkRenderPassManager::IEvictionObserver: the render-pass aging sweep is
|
||||
// destroying a VkRenderPass; evict every graphics pipeline hashed on the
|
||||
// VkRenderPassManager::IEvictionObserver: the render-pass aging sweep just
|
||||
// destroyed these VkRenderPasses; evict every graphics pipeline hashed on a
|
||||
// dying handle (they share its >1024-boundary idleness, so immediate
|
||||
// destruction is safe) and drop the last-pipeline memo if any went.
|
||||
void OnRenderPassDestroyed(VkRenderPass renderPass) override;
|
||||
void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) override;
|
||||
|
||||
// ProgramFactory::IEvictionObserver: an aged-out program entry was
|
||||
// destroyed; evict its compute pipeline and graphics pipelines (same
|
||||
@@ -376,6 +376,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkFence> m_freeSubmitFences;
|
||||
Uint64 m_submitCounter = 0;
|
||||
Uint64 m_completedSubmitCounter = 0;
|
||||
// Drains since the last Present, gating the drain's frame-boundary-equivalent
|
||||
// work (arena rewind + cache aging): a presenting app's mid-frame
|
||||
// readbacks/waits must neither churn the transient caches nor accelerate the
|
||||
// aging clocks, while present-less loops still cross a boundary every few
|
||||
// iterations. Reset in Present.
|
||||
Uint32 m_drainsSinceLastPresent = 0;
|
||||
|
||||
NativeWindowType m_window = 0;
|
||||
void* m_platformDisplay = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user