mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix] (DirectVulkan): bound the vertex-input and sampler caches and sweep undeleted GL syncs - both caches age out entries idle >1024 frame boundaries (animated LOD bias no longer mints a VkSampler per float value, buffer/VAO churn no longer grows the vertex-input map for the whole session), and library teardown drains the live-sync registry exactly as glDeleteSync would since GL requires syncs to die with their context
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
#include <MG_State/EGLState/Core.h>
|
#include <MG_State/EGLState/Core.h>
|
||||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||||
|
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -37,6 +38,12 @@ namespace MobileGL {
|
|||||||
MGLOG_I("MobileGL closing...");
|
MGLOG_I("MobileGL closing...");
|
||||||
}
|
}
|
||||||
glslang::FinalizeProcess();
|
glslang::FinalizeProcess();
|
||||||
|
// GL syncs die with their contexts, and every context is gone by the
|
||||||
|
// time full teardown runs: drain the live-sync registry while the
|
||||||
|
// backend function table can still release the backend handles (and
|
||||||
|
// before a re-initialized library could pair them with the wrong
|
||||||
|
// backend's DeleteSync).
|
||||||
|
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||||
MG_Backend::pActiveBackendObject.reset();
|
MG_Backend::pActiveBackendObject.reset();
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
MG_State::pEGLContext.reset();
|
MG_State::pEGLContext.reset();
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||||
|
|
||||||
|
// The buffer's heap address is an identity component of the key: a freed
|
||||||
|
// buffer's reused address can alias an old cache entry, but only under a
|
||||||
|
// byte-identical attribute layout - and the entry payload is a pure function
|
||||||
|
// of the hashed inputs, with the draw path re-resolving bindingBufferKeys
|
||||||
|
// against the live VAO attribute pointers, so an aliased hit returns exactly
|
||||||
|
// what a rebuild would. Address drift only grows the map; the OnFrameBoundary
|
||||||
|
// aging sweep bounds that.
|
||||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||||
}
|
}
|
||||||
@@ -58,6 +65,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
|
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
auto& entry = m_cache[hash];
|
auto& entry = m_cache[hash];
|
||||||
entry.hash = hash;
|
entry.hash = hash;
|
||||||
|
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
entry.bindings = builder.GetBindings();
|
entry.bindings = builder.GetBindings();
|
||||||
entry.attributes = builder.GetAttributes();
|
entry.attributes = builder.GetAttributes();
|
||||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||||
@@ -180,6 +189,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VertexInputStateFactory::OnFrameBoundary() {
|
||||||
|
++m_frameBoundaryCounter;
|
||||||
|
|
||||||
|
// Sweep occasionally; evict entries whose last hit is far in the past.
|
||||||
|
// Erasure happens only here, never mid-frame: the draw path holds a
|
||||||
|
// reference into the current entry across its setup, and unordered_map
|
||||||
|
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
|
||||||
|
// proof is needed; an evicted entry that is used again is simply rebuilt
|
||||||
|
// from the VAO state (same hash, same content).
|
||||||
|
constexpr Uint64 kSweepInterval = 256;
|
||||||
|
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||||
|
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||||
|
if (m_frameBoundaryCounter - it->second.lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||||
|
it = m_cache.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||||
Bool isBgra) {
|
Bool isBgra) {
|
||||||
if (isBgra) {
|
if (isBgra) {
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
|
// Frame boundary of the last cache hit; entries idle past the
|
||||||
|
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
||||||
|
Uint64 lastUsedFrameBoundary = 0;
|
||||||
Vector<VkVertexInputBindingDescription> bindings;
|
Vector<VkVertexInputBindingDescription> bindings;
|
||||||
Vector<VkVertexInputAttributeDescription> attributes;
|
Vector<VkVertexInputAttributeDescription> attributes;
|
||||||
Vector<SizeT> bindingBufferKeys;
|
Vector<SizeT> bindingBufferKeys;
|
||||||
@@ -55,6 +58,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||||
|
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
||||||
|
// frames. The key mixes buffer heap addresses, so buffer/VAO churn keeps
|
||||||
|
// minting fresh keys; without eviction the map grows for the whole session.
|
||||||
|
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
||||||
|
// and the draw path's entry reference never spans a frame boundary, so
|
||||||
|
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
||||||
|
// compare except on sweep boundaries.
|
||||||
|
void OnFrameBoundary();
|
||||||
static SizeT GetComponentSize(DataType type);
|
static SizeT GetComponentSize(DataType type);
|
||||||
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
||||||
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
||||||
@@ -70,6 +81,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||||
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
|
Uint64 m_frameBoundaryCounter = 0;
|
||||||
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
|
||||||
|
|||||||
@@ -89,6 +89,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
m_device = VK_NULL_HANDLE;
|
m_device = VK_NULL_HANDLE;
|
||||||
m_config = nullptr;
|
m_config = nullptr;
|
||||||
|
m_frameBoundaryCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkSamplerManager::OnFrameBoundary() {
|
||||||
|
++m_frameBoundaryCounter;
|
||||||
|
|
||||||
|
// Sweep occasionally; destroy samplers whose last use is far past every
|
||||||
|
// in-flight frame. Destroy and erase must stay atomic, or Shutdown would
|
||||||
|
// double-free the handle; an evicted key that recurs simply re-creates
|
||||||
|
// its sampler on the next miss.
|
||||||
|
constexpr Uint64 kSweepInterval = 256;
|
||||||
|
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||||
|
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = m_samplers.begin(); it != m_samplers.end();) {
|
||||||
|
auto& entry = it->second;
|
||||||
|
if (m_frameBoundaryCounter - entry.lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||||
|
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||||
|
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||||
|
}
|
||||||
|
it = m_samplers.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||||
@@ -137,6 +164,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
|
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
|
||||||
auto it = m_samplers.find(key);
|
auto it = m_samplers.find(key);
|
||||||
if (it != m_samplers.end()) {
|
if (it != m_samplers.end()) {
|
||||||
|
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
return it->second.handle;
|
return it->second.handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +197,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
entry.handle = vkSampler;
|
entry.handle = vkSampler;
|
||||||
entry.externalIndex = sampler.GetExternalIndex();
|
entry.externalIndex = sampler.GetExternalIndex();
|
||||||
entry.version = sampler.GetVersion();
|
entry.version = sampler.GetVersion();
|
||||||
|
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
m_samplers[key] = entry;
|
m_samplers[key] = entry;
|
||||||
return vkSampler;
|
return vkSampler;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,27 @@ public:
|
|||||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture,
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
Bool forceNearestFiltering = false);
|
Bool forceNearestFiltering = false);
|
||||||
|
// Frame boundary hook: ages the sampler cache and destroys samplers not used
|
||||||
|
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
|
||||||
|
// anisotropy), so an app animating those would otherwise mint an unbounded
|
||||||
|
// stream of never-destroyed VkSamplers and eventually exhaust the device's
|
||||||
|
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
|
||||||
|
// boundaries cannot be referenced by any in-flight command buffer (frames in
|
||||||
|
// flight are single digits), and every descriptor set the GPU consumes is
|
||||||
|
// written that same frame with live handles (the per-binding resolve memo and
|
||||||
|
// descriptor-set reuse are both frame-reset), so destruction here needs no
|
||||||
|
// fence wait. Self-gated: one counter bump and compare except on sweep
|
||||||
|
// boundaries.
|
||||||
|
void OnFrameBoundary();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct SamplerCacheEntry {
|
struct SamplerCacheEntry {
|
||||||
VkSampler handle = VK_NULL_HANDLE;
|
VkSampler handle = VK_NULL_HANDLE;
|
||||||
Uint externalIndex = 0;
|
Uint externalIndex = 0;
|
||||||
Uint16 version = 0;
|
Uint16 version = 0;
|
||||||
|
// Frame boundary of the last cache hit; entries idle past the
|
||||||
|
// OnFrameBoundary retirement age have their VkSampler destroyed.
|
||||||
|
Uint64 lastUsedFrameBoundary = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||||
@@ -67,6 +82,8 @@ private:
|
|||||||
Bool m_samplerAnisotropySupported = false;
|
Bool m_samplerAnisotropySupported = false;
|
||||||
Float m_maxSamplerAnisotropy = 1.0f;
|
Float m_maxSamplerAnisotropy = 1.0f;
|
||||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||||
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
|
Uint64 m_frameBoundaryCounter = 0;
|
||||||
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
|
||||||
|
|||||||
@@ -7269,6 +7269,12 @@ void main() {
|
|||||||
if (m_uniformManager) {
|
if (m_uniformManager) {
|
||||||
m_uniformManager->OnFrameBoundary();
|
m_uniformManager->OnFrameBoundary();
|
||||||
}
|
}
|
||||||
|
if (m_vertexInputStateFactory) {
|
||||||
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
|
}
|
||||||
|
if (m_samplerManager) {
|
||||||
|
m_samplerManager->OnFrameBoundary();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7534,6 +7540,8 @@ void main() {
|
|||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
m_uniformManager->OnFrameBoundary();
|
m_uniformManager->OnFrameBoundary();
|
||||||
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
|
m_samplerManager->OnFrameBoundary();
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||||
if (activeRenderPass)
|
if (activeRenderPass)
|
||||||
|
|||||||
@@ -133,4 +133,33 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
values[0] = value;
|
values[0] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DestroyAllSyncObjects() {
|
||||||
|
// Detach the registry under the lock, release outside it. Entries the app
|
||||||
|
// already deleted were erased by DeleteSync, so nothing here double-frees;
|
||||||
|
// a DeleteSync racing this sweep finds an empty registry and returns. A
|
||||||
|
// thread still blocked inside ClientWaitSync/GetSynciv during teardown
|
||||||
|
// holds a raw SyncObject* these deletes invalidate - the same undefined
|
||||||
|
// race an app-driven DeleteSync already has.
|
||||||
|
UnorderedMap<GLsync, SyncObject*> orphans;
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||||
|
orphans.swap(g_liveSyncObjects);
|
||||||
|
}
|
||||||
|
if (orphans.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Both backends' DeleteSync only free the heap wrapper once their GL
|
||||||
|
// context/renderer is gone (generation/current-thread guards), so this is
|
||||||
|
// safe after the backend has released its EGL resources - but not after
|
||||||
|
// the function table itself is cleared.
|
||||||
|
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||||
|
for (const auto& [_, syncObject] : orphans) {
|
||||||
|
if (backendDeleteSync && syncObject->backendHandle) {
|
||||||
|
backendDeleteSync(syncObject->backendHandle);
|
||||||
|
}
|
||||||
|
delete syncObject;
|
||||||
|
}
|
||||||
|
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -16,4 +16,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
|
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
|
||||||
void DeleteSync(GLsync sync);
|
void DeleteSync(GLsync sync);
|
||||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
|
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
|
||||||
|
// Destroys every still-registered sync object exactly as DeleteSync would.
|
||||||
|
// GL requires syncs to die with their context; called only from full library
|
||||||
|
// teardown (DestroyImpl), where no context survives on any thread, so the
|
||||||
|
// process-global registry can be drained wholesale. Must run while the
|
||||||
|
// backend function table is still populated: each backend handle has to be
|
||||||
|
// released by the backend that created it, never by a later re-initialized
|
||||||
|
// one.
|
||||||
|
void DestroyAllSyncObjects();
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
Reference in New Issue
Block a user