mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix] (DirectVulkan): age-based eviction for the content-addressed cache family - ProgramFactory entries (shader modules/layouts), PipelineFactory graphics pipelines, compute pipelines and per-layout descriptor-set tracking now retire after ~1024 idle frame boundaries (render-pass-manager sweep precedent), render-pass eviction purges pipelines hashed on the dying handle (closes a handle-recycling stale-pipeline hazard), and the program reflection cache is lifetime-id-keyed and cleared at EGL teardown - shader/program churn no longer grows Vulkan objects without bound
This commit is contained in:
@@ -469,6 +469,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// treat them as signaled/available with zero results from here on.
|
// treat them as signaled/available with zero results from here on.
|
||||||
BumpRendererGeneration();
|
BumpRendererGeneration();
|
||||||
pVulkanRenderer.reset();
|
pVulkanRenderer.reset();
|
||||||
|
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||||
|
// deleted programs' reflection strings survive full context teardown.
|
||||||
|
ClearProgramResourceCaches();
|
||||||
BackendObject::ReleaseEGLResources();
|
BackendObject::ReleaseEGLResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,6 +481,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// treat them as signaled/available with zero results from here on.
|
// treat them as signaled/available with zero results from here on.
|
||||||
BumpRendererGeneration();
|
BumpRendererGeneration();
|
||||||
pVulkanRenderer.reset();
|
pVulkanRenderer.reset();
|
||||||
|
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||||
|
// deleted programs' reflection strings survive full context teardown.
|
||||||
|
ClearProgramResourceCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
|
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct ProgramResourceCache {
|
struct ProgramResourceCache {
|
||||||
|
// Lifetime id of the program the cached reflection belongs to. GL names are
|
||||||
|
// recycled (IndexGenerator hands freed indices straight back), and a
|
||||||
|
// recreated program's backendStateVersion restarts at the same small values,
|
||||||
|
// so the version alone can collide; the never-reused lifetime id makes the
|
||||||
|
// slot's ownership unambiguous.
|
||||||
|
Uint64 programLifetimeId = 0;
|
||||||
Uint32 backendStateVersion = 0;
|
Uint32 backendStateVersion = 0;
|
||||||
Vector<StorageBlockResource> storageBlocks;
|
Vector<StorageBlockResource> storageBlocks;
|
||||||
Vector<BufferVariableResource> bufferVariables;
|
Vector<BufferVariableResource> bufferVariables;
|
||||||
@@ -82,6 +88,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 baseInstance = 0;
|
Uint32 baseInstance = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keyed by GL program name so the freed-name reuse in IndexGenerator bounds the
|
||||||
|
// map at the peak-simultaneous-program high-water mark; each slot's ownership is
|
||||||
|
// checked against the program's lifetime id before it is served (see
|
||||||
|
// GetProgramResourceCache). Cleared wholesale at EGL teardown via
|
||||||
|
// ClearProgramResourceCaches.
|
||||||
UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;
|
UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;
|
||||||
|
|
||||||
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||||
@@ -142,13 +153,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) {
|
ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) {
|
||||||
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
||||||
|
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||||
if (cache.backendStateVersion == backendStateVersion &&
|
// The lifetime id must match too: a new program that reuses a deleted
|
||||||
|
// program's name and happens to land on the same backendStateVersion (both
|
||||||
|
// count from zero) would otherwise be served the dead program's reflection.
|
||||||
|
if (cache.programLifetimeId == programLifetimeId &&
|
||||||
|
cache.backendStateVersion == backendStateVersion &&
|
||||||
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
cache = {};
|
cache = {};
|
||||||
|
cache.programLifetimeId = programLifetimeId;
|
||||||
cache.backendStateVersion = backendStateVersion;
|
cache.backendStateVersion = backendStateVersion;
|
||||||
|
|
||||||
Vector<SpvReflectShaderModule> modules;
|
Vector<SpvReflectShaderModule> modules;
|
||||||
@@ -366,6 +383,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void ClearProgramResourceCaches() {
|
||||||
|
// Called from EGL teardown while the backend's m_eglStateMutex is held; GL
|
||||||
|
// calls are serialized in this codebase (contexts migrate threads but never
|
||||||
|
// run concurrently), so no other thread can be inside the unsynchronized map.
|
||||||
|
// Live programs in another context self-heal: their entry rebuilds from the
|
||||||
|
// retained generated SPIR-V on the next resource query.
|
||||||
|
g_programResourceCaches.clear();
|
||||||
|
}
|
||||||
|
|
||||||
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||||
auto& cache = GetProgramResourceCache(program);
|
auto& cache = GetProgramResourceCache(program);
|
||||||
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 GetRendererGeneration();
|
Uint64 GetRendererGeneration();
|
||||||
void BumpRendererGeneration();
|
void BumpRendererGeneration();
|
||||||
|
|
||||||
|
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
|
||||||
|
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
|
||||||
|
// safe because GL calls are serialized in this codebase, and any still-live
|
||||||
|
// program rebuilds its entry from the retained generated SPIR-V on demand.
|
||||||
|
void ClearProgramResourceCaches();
|
||||||
|
|
||||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||||
|
|||||||
@@ -243,23 +243,100 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const HashType hash = ComputeHash(payload);
|
const HashType hash = ComputeHash(payload);
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
return it->second;
|
it->second.lastUsedFrame = m_frameCounter;
|
||||||
|
return it->second.pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkPipeline pipeline = CreatePipeline(payload);
|
VkPipeline pipeline = CreatePipeline(payload);
|
||||||
m_cache.emplace(hash, pipeline);
|
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
||||||
|
m_frameCounter});
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PipelineFactory::DestroyAll() {
|
void PipelineFactory::DestroyAll() {
|
||||||
for (auto& pair : m_cache) {
|
for (auto& pair : m_cache) {
|
||||||
if (pair.second != VK_NULL_HANDLE) {
|
if (pair.second.pipeline != VK_NULL_HANDLE) {
|
||||||
vkDestroyPipeline(m_device, pair.second, nullptr);
|
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_cache.clear();
|
m_cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint32 PipelineFactory::OnFrameBoundary() {
|
||||||
|
++m_frameCounter;
|
||||||
|
|
||||||
|
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
|
||||||
|
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
|
||||||
|
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
|
||||||
|
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
|
||||||
|
// pipeline" memo when this returns non-zero: the memo can return a cached
|
||||||
|
// handle without touching this cache, so an evicted pipeline may still be
|
||||||
|
// memoized (present-less flush loops never reset the memo per frame).
|
||||||
|
constexpr Uint64 kSweepInterval = 256;
|
||||||
|
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||||
|
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 evicted = 0;
|
||||||
|
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||||
|
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||||
|
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||||
|
}
|
||||||
|
it = m_cache.erase(it);
|
||||||
|
++evicted;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (evicted > 0) {
|
||||||
|
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
|
||||||
|
m_cache.size());
|
||||||
|
}
|
||||||
|
return evicted;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 PipelineFactory::EvictByRenderPass(VkRenderPass renderPass) {
|
||||||
|
Uint32 evicted = 0;
|
||||||
|
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||||
|
if (it->second.renderPass == renderPass) {
|
||||||
|
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||||
|
}
|
||||||
|
it = m_cache.erase(it);
|
||||||
|
++evicted;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (evicted > 0) {
|
||||||
|
MGLOG_D("PipelineFactory::EvictByRenderPass: evicted %u pipelines for destroyed render pass",
|
||||||
|
evicted);
|
||||||
|
}
|
||||||
|
return evicted;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
|
||||||
|
Uint32 evicted = 0;
|
||||||
|
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||||
|
if (it->second.programHash == programHash) {
|
||||||
|
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||||
|
}
|
||||||
|
it = m_cache.erase(it);
|
||||||
|
++evicted;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (evicted > 0) {
|
||||||
|
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
|
||||||
|
evicted, static_cast<unsigned long long>(programHash));
|
||||||
|
}
|
||||||
|
return evicted;
|
||||||
|
}
|
||||||
|
|
||||||
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
|
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
|
||||||
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
|
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
|
||||||
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
|
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
|
||||||
|
|||||||
@@ -65,6 +65,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||||
void DestroyAll();
|
void DestroyAll();
|
||||||
|
|
||||||
|
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
|
||||||
|
// (their command buffers retired many frames ago), mirroring
|
||||||
|
// 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 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
|
||||||
|
// that stamp its factory entry). Returns the number destroyed.
|
||||||
|
Uint32 EvictByProgramHash(HashType programHash);
|
||||||
|
|
||||||
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
||||||
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
||||||
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
||||||
@@ -87,12 +107,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
|
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct PipelineCacheEntry {
|
||||||
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
|
// The hashed inputs the eviction paths key on: programHash ties the entry to
|
||||||
|
// its ProgramFactory entry, renderPass records the exact handle the hash
|
||||||
|
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
|
||||||
|
HashType programHash = 0;
|
||||||
|
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||||
|
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
|
||||||
|
// cache eviction (see OnFrameBoundary).
|
||||||
|
Uint64 lastUsedFrame = 0;
|
||||||
|
};
|
||||||
|
|
||||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||||
|
|
||||||
VkDevice m_device = VK_NULL_HANDLE;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||||
UnorderedMap<HashType, VkPipeline> m_cache;
|
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||||
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
|
Uint64 m_frameCounter = 0;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1950,11 +1950,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
|
// Every draw/dispatch funnels through this lookup (the renderer memos only
|
||||||
|
// skip re-hashing, never the factory lookup), so an actively-used entry is
|
||||||
|
// stamped at least once per frame boundary and can never be aged out while
|
||||||
|
// any in-flight command buffer still references it.
|
||||||
|
it->second.lastUsedFrame = m_frameCounter;
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& entry = m_cache[hash];
|
auto& entry = m_cache[hash];
|
||||||
entry.hash = hash;
|
entry.hash = hash;
|
||||||
|
entry.lastUsedFrame = m_frameCounter;
|
||||||
auto& shaders = program.GetAttachedShaders();
|
auto& shaders = program.GetAttachedShaders();
|
||||||
auto& spirv = program.GetGeneratedSpirv();
|
auto& spirv = program.GetGeneratedSpirv();
|
||||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||||
@@ -2068,4 +2074,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProgramFactory::OnFrameBoundary() {
|
||||||
|
++m_frameCounter;
|
||||||
|
|
||||||
|
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
|
||||||
|
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
|
||||||
|
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
|
||||||
|
// so its shader modules and layouts are destroyed immediately - no deferred-
|
||||||
|
// destroy machinery needed. Eviction is content-based, never tied to
|
||||||
|
// glDeleteProgram: the cache is content-hash-shared across GL programs, so a
|
||||||
|
// delete-driven erase could free an entry another live program still resolves.
|
||||||
|
// An evicted entry self-heals - the frontend program keeps its generated
|
||||||
|
// SPIR-V, so the next GetOrCreateProgram rebuilds it (this also covers the
|
||||||
|
// renderer's internal blit/depth-mipmap programs).
|
||||||
|
constexpr Uint64 kSweepInterval = 256;
|
||||||
|
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||||
|
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||||
|
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||||
|
const HashType hash = it->first;
|
||||||
|
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
|
||||||
|
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
|
||||||
|
static_cast<unsigned long long>(hash));
|
||||||
|
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
|
||||||
|
// so an observer never observes a half-destroyed entry through a lookup.
|
||||||
|
// Observers only need the handle values to purge their keyed caches.
|
||||||
|
it = m_cache.erase(it);
|
||||||
|
if (m_evictionObserver != nullptr) {
|
||||||
|
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||||
Bool fragmentReplacesDepth = false;
|
Bool fragmentReplacesDepth = false;
|
||||||
|
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||||
|
// cache eviction (see OnFrameBoundary).
|
||||||
|
Uint64 lastUsedFrame = 0;
|
||||||
|
|
||||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||||
|
|
||||||
@@ -124,6 +127,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
@@ -135,6 +139,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
|
other.lastUsedFrame = 0;
|
||||||
}
|
}
|
||||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||||
if (this == &other) {
|
if (this == &other) {
|
||||||
@@ -170,6 +175,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
@@ -181,6 +187,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
|
other.lastUsedFrame = 0;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +217,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
|
||||||
|
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
|
||||||
|
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
|
||||||
|
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
|
||||||
|
// layout handle value may be recycled for an unrelated layout, and the program
|
||||||
|
// hash may be re-inserted by a later rebuild of the same content.
|
||||||
|
class IEvictionObserver {
|
||||||
|
public:
|
||||||
|
virtual ~IEvictionObserver() = default;
|
||||||
|
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||||
Bool shaderDrawParametersEnabled = false,
|
Bool shaderDrawParametersEnabled = false,
|
||||||
Bool unformattedFloatStorageImagesEnabled = false)
|
Bool unformattedFloatStorageImagesEnabled = false)
|
||||||
@@ -225,6 +244,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkProgramObject& GetOrCreateProgram(
|
const VkProgramObject& GetOrCreateProgram(
|
||||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||||
|
|
||||||
|
// Observer may be null (no notifications). Not owned.
|
||||||
|
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||||
|
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
||||||
|
// (their command buffers retired many frames ago), mirroring
|
||||||
|
// VkRenderPassManager::OnPresent's sweep.
|
||||||
|
void OnFrameBoundary();
|
||||||
|
|
||||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||||
@@ -266,6 +292,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
mutable ProgramLookupCache m_lastLookup;
|
mutable ProgramLookupCache m_lastLookup;
|
||||||
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
|
Uint64 m_frameCounter = 0;
|
||||||
|
IEvictionObserver* m_evictionObserver = nullptr;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_programFactory = nullptr;
|
m_programFactory = nullptr;
|
||||||
m_device = VK_NULL_HANDLE;
|
m_device = VK_NULL_HANDLE;
|
||||||
m_minDynamicOffsetAlignment = 1;
|
m_minDynamicOffsetAlignment = 1;
|
||||||
|
m_frameCounter = 0;
|
||||||
m_frameCount = 0;
|
m_frameCount = 0;
|
||||||
m_maxBindings = 0;
|
m_maxBindings = 0;
|
||||||
m_setsPerFrame = 0;
|
m_setsPerFrame = 0;
|
||||||
@@ -211,6 +212,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
|
||||||
|
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 (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
@@ -989,6 +1040,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkDescriptorSet& outDescriptorSet) {
|
VkDescriptorSet& outDescriptorSet) {
|
||||||
auto& frame = m_frames[frameIndex];
|
auto& frame = m_frames[frameIndex];
|
||||||
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
|
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()) {
|
if (cache.cursor < cache.sets.size()) {
|
||||||
outDescriptorSet = cache.sets[cache.cursor++];
|
outDescriptorSet = cache.sets[cache.cursor++];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
|
||||||
void BeginFrame(Uint32 frameIndex);
|
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.
|
||||||
|
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,
|
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
||||||
@@ -68,6 +80,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
struct DescriptorSetCacheEntry {
|
struct DescriptorSetCacheEntry {
|
||||||
Vector<VkDescriptorSet> sets;
|
Vector<VkDescriptorSet> sets;
|
||||||
Uint32 cursor = 0;
|
Uint32 cursor = 0;
|
||||||
|
// Frame-boundary counter value of the last AcquireDescriptorSet hit; drives
|
||||||
|
// cache eviction (see OnFrameBoundary).
|
||||||
|
Uint64 lastUsedFrame = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FrameResources {
|
struct FrameResources {
|
||||||
@@ -131,6 +146,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<FrameResources> m_frames;
|
Vector<FrameResources> m_frames;
|
||||||
|
|
||||||
VkDeviceSize m_minDynamicOffsetAlignment = 1;
|
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_frameCount = 0;
|
||||||
Uint32 m_maxBindings = 0;
|
Uint32 m_maxBindings = 0;
|
||||||
Uint32 m_setsPerFrame = 0;
|
Uint32 m_setsPerFrame = 0;
|
||||||
|
|||||||
@@ -1194,6 +1194,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
||||||
m_rpFastValid = false;
|
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);
|
||||||
|
}
|
||||||
it = m_renderPasses.erase(it);
|
it = m_renderPasses.erase(it);
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
|
|||||||
@@ -157,11 +157,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
class VkRenderPassManager {
|
class VkRenderPassManager {
|
||||||
public:
|
public:
|
||||||
using HashType = Uint64;
|
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.
|
||||||
|
class IEvictionObserver {
|
||||||
|
public:
|
||||||
|
virtual ~IEvictionObserver() = default;
|
||||||
|
virtual void OnRenderPassDestroyed(VkRenderPass renderPass) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
VkRenderPassManager(VkDevice device,
|
VkRenderPassManager(VkDevice device,
|
||||||
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
||||||
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
||||||
~VkRenderPassManager();
|
~VkRenderPassManager();
|
||||||
|
|
||||||
|
// Observer may be null (no notifications). Not owned.
|
||||||
|
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||||
|
|
||||||
Bool Initialize();
|
Bool Initialize();
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
|
||||||
@@ -192,6 +208,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
||||||
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
||||||
Uint64 m_frameCounter = 0;
|
Uint64 m_frameCounter = 0;
|
||||||
|
IEvictionObserver* m_evictionObserver = nullptr;
|
||||||
|
|
||||||
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
||||||
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
||||||
|
|||||||
@@ -2529,6 +2529,11 @@ void main() {
|
|||||||
m_shaderDrawParametersFeatureEnabled,
|
m_shaderDrawParametersFeatureEnabled,
|
||||||
m_unformattedFloatStorageImagesEnabled);
|
m_unformattedFloatStorageImagesEnabled);
|
||||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||||
|
// Aging evictions (render passes and program entries) must purge the dependent
|
||||||
|
// pipeline / compute-pipeline / descriptor-set caches in the same step; both
|
||||||
|
// sweeps only run from the frame-boundary seams, long after initialization.
|
||||||
|
m_renderPassManager->SetEvictionObserver(this);
|
||||||
|
m_programFactory->SetEvictionObserver(this);
|
||||||
|
|
||||||
m_samplerManager = MakeUnique<VkSamplerManager>();
|
m_samplerManager = MakeUnique<VkSamplerManager>();
|
||||||
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
|
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
|
||||||
@@ -2589,6 +2594,15 @@ void main() {
|
|||||||
DestroyDeferredDepthMipmapCleanup();
|
DestroyDeferredDepthMipmapCleanup();
|
||||||
DestroyComputePipelines();
|
DestroyComputePipelines();
|
||||||
|
|
||||||
|
// No sweep runs during teardown, but the observers point at this renderer
|
||||||
|
// and the factories die at different times below; disconnect them first.
|
||||||
|
if (m_renderPassManager) {
|
||||||
|
m_renderPassManager->SetEvictionObserver(nullptr);
|
||||||
|
}
|
||||||
|
if (m_programFactory) {
|
||||||
|
m_programFactory->SetEvictionObserver(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
m_pipelineFactory.reset();
|
m_pipelineFactory.reset();
|
||||||
ShutdownBlitResources();
|
ShutdownBlitResources();
|
||||||
ShutdownDepthMipmapResources();
|
ShutdownDepthMipmapResources();
|
||||||
@@ -7241,6 +7255,20 @@ void main() {
|
|||||||
if (m_renderPassManager) {
|
if (m_renderPassManager) {
|
||||||
m_renderPassManager->OnPresent();
|
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.
|
||||||
|
if (m_programFactory) {
|
||||||
|
m_programFactory->OnFrameBoundary();
|
||||||
|
}
|
||||||
|
if (m_pipelineFactory && m_pipelineFactory->OnFrameBoundary() > 0) {
|
||||||
|
m_lastPipelineValid = false;
|
||||||
|
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (m_uniformManager) {
|
||||||
|
m_uniformManager->OnFrameBoundary();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7496,6 +7524,16 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
|
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
|
||||||
"Present, acquired image index out of range");
|
"Present, acquired image index out of range");
|
||||||
m_renderPassManager->OnPresent();
|
m_renderPassManager->OnPresent();
|
||||||
|
// 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.
|
||||||
|
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();
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||||
if (activeRenderPass)
|
if (activeRenderPass)
|
||||||
@@ -8528,6 +8566,43 @@ void main() {
|
|||||||
m_computePipelines.clear();
|
m_computePipelines.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VulkanRenderer::OnRenderPassDestroyed(VkRenderPass renderPass) {
|
||||||
|
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
|
||||||
|
// 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) {
|
||||||
|
m_lastPipelineValid = false;
|
||||||
|
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanRenderer::OnProgramEvicted(ProgramFactory::HashType programHash,
|
||||||
|
VkDescriptorSetLayout descriptorSetLayout) {
|
||||||
|
// Same >1024-boundary idleness as the program entry: its compute pipeline is
|
||||||
|
// only dispatched, and its graphics pipelines only bound, through paths that
|
||||||
|
// stamp the entry, so immediate destruction is GPU-safe. (The graphics memo
|
||||||
|
// never holds compute pipelines; it only needs invalidating for the factory
|
||||||
|
// eviction below.)
|
||||||
|
const auto computeIt = m_computePipelines.find(programHash);
|
||||||
|
if (computeIt != m_computePipelines.end()) {
|
||||||
|
if (computeIt->second != VK_NULL_HANDLE && m_device != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyPipeline(m_device, computeIt->second, nullptr);
|
||||||
|
}
|
||||||
|
m_computePipelines.erase(computeIt);
|
||||||
|
}
|
||||||
|
if (m_pipelineFactory != nullptr && m_pipelineFactory->EvictByProgramHash(programHash) > 0) {
|
||||||
|
m_lastPipelineValid = false;
|
||||||
|
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (m_uniformManager != nullptr) {
|
||||||
|
m_uniformManager->OnDescriptorSetLayoutDestroyed(descriptorSetLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VkPipeline VulkanRenderer::GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj) {
|
VkPipeline VulkanRenderer::GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj) {
|
||||||
const auto it = m_computePipelines.find(programObj.hash);
|
const auto it = m_computePipelines.find(programObj.hash);
|
||||||
if (it != m_computePipelines.end()) {
|
if (it != m_computePipelines.end()) {
|
||||||
|
|||||||
@@ -114,7 +114,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
|
class VulkanRenderer : public IBufferCopyCommandProvider,
|
||||||
|
public FrameContext::IRecordingObserver,
|
||||||
|
public VkRenderPassManager::IEvictionObserver,
|
||||||
|
public ProgramFactory::IEvictionObserver {
|
||||||
public:
|
public:
|
||||||
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
||||||
~VulkanRenderer();
|
~VulkanRenderer();
|
||||||
@@ -131,6 +134,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// recording, before any render pass.
|
// recording, before any render pass.
|
||||||
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
|
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
|
||||||
|
|
||||||
|
// VkRenderPassManager::IEvictionObserver: the render-pass aging sweep is
|
||||||
|
// destroying a VkRenderPass; evict every graphics pipeline hashed on the
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// ProgramFactory::IEvictionObserver: an aged-out program entry was
|
||||||
|
// destroyed; evict its compute pipeline and graphics pipelines (same
|
||||||
|
// idleness guarantee - they are only bound through draws/dispatches that
|
||||||
|
// stamp the program entry) and purge the descriptor-set cache entries
|
||||||
|
// keyed by its now-recyclable VkDescriptorSetLayout handle.
|
||||||
|
void OnProgramEvicted(ProgramFactory::HashType programHash,
|
||||||
|
VkDescriptorSetLayout descriptorSetLayout) override;
|
||||||
|
|
||||||
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||||
const DrawCmdParam& drawParams,
|
const DrawCmdParam& drawParams,
|
||||||
const IndexBufferView* pIndexBufferView = nullptr);
|
const IndexBufferView* pIndexBufferView = nullptr);
|
||||||
|
|||||||
Reference in New Issue
Block a user