mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
Compare commits
6
Commits
6ae3245a0d
...
56b31a9587
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56b31a9587 | ||
|
|
8a0a8a0274 | ||
|
|
d076c29146 | ||
|
|
930a607bdf | ||
|
|
34685b4bb0 | ||
|
|
c540fb88ee |
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -264,7 +264,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
|
// lastSubmitIndex was just written by the renderer for the submission
|
||||||
|
// that carried this command buffer.
|
||||||
|
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
|
||||||
frame.commandBuffer = replacement;
|
frame.commandBuffer = replacement;
|
||||||
return VK_SUCCESS;
|
return VK_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -274,12 +276,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
|
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
|
||||||
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
|
for (const auto& retired : frame.retiredCommandBuffers) {
|
||||||
frame.retiredCommandBuffers.data());
|
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frame.retiredCommandBuffers.clear();
|
frame.retiredCommandBuffers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
|
||||||
|
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (auto& frame : m_frames) {
|
||||||
|
// Retired buffers are appended in submit order, so the completed
|
||||||
|
// ones form a prefix.
|
||||||
|
SizeT completedCount = 0;
|
||||||
|
while (completedCount < frame.retiredCommandBuffers.size() &&
|
||||||
|
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
|
||||||
|
vkFreeCommandBuffers(m_device, m_commandPool, 1,
|
||||||
|
&frame.retiredCommandBuffers[completedCount].commandBuffer);
|
||||||
|
++completedCount;
|
||||||
|
}
|
||||||
|
if (completedCount > 0) {
|
||||||
|
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
|
||||||
|
frame.retiredCommandBuffers.begin() + completedCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameContext::FreeAllRetiredCommandBuffers() {
|
||||||
|
for (auto& frame : m_frames) {
|
||||||
|
FreeRetiredCommandBuffers(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
|
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
|
||||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
|
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
|
||||||
|
// with the submit-tracker index it was submitted under so it can be
|
||||||
|
// freed as soon as that submission is observed complete - without
|
||||||
|
// waiting for the slot's fence to be waited again (present-less flush
|
||||||
|
// loops never wait it).
|
||||||
|
struct RetiredCommandBuffer {
|
||||||
|
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||||
|
Uint64 submitIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct FrameData {
|
struct FrameData {
|
||||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||||
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
||||||
@@ -47,10 +57,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool isCommandRecording = false;
|
Bool isCommandRecording = false;
|
||||||
Bool hasCommandBufferRecorded = false;
|
Bool hasCommandBufferRecorded = false;
|
||||||
Bool imageAvailableSemaphoreConsumed = false;
|
Bool imageAvailableSemaphoreConsumed = false;
|
||||||
// Command buffers submitted mid-frame (FlushPendingCommands) whose
|
// Command buffers submitted mid-frame (FlushPendingCommands),
|
||||||
// execution is only known complete once this slot's fence has been
|
// appended in submit order; freed once their submission is known
|
||||||
// waited again; freed at that point.
|
// complete (fence wait or completion poll).
|
||||||
Vector<VkCommandBuffer> retiredCommandBuffers;
|
Vector<RetiredCommandBuffer> retiredCommandBuffers;
|
||||||
// Submit-tracker index of this slot's most recent queue submission
|
// Submit-tracker index of this slot's most recent queue submission
|
||||||
// (written by the renderer at submit time).
|
// (written by the renderer at submit time).
|
||||||
Uint64 lastSubmitIndex = 0;
|
Uint64 lastSubmitIndex = 0;
|
||||||
@@ -79,9 +89,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Parks the current (already ended and submitted) command buffer on the
|
// Parks the current (already ended and submitted) command buffer on the
|
||||||
// slot's retired list and installs a freshly allocated one, so recording
|
// slot's retired list and installs a freshly allocated one, so recording
|
||||||
// can restart while the submitted buffer is still executing. Retired
|
// can restart while the submitted buffer is still executing. Retired
|
||||||
// buffers are freed after the slot's fence is next waited.
|
// buffers are freed after the slot's fence is next waited, or as soon
|
||||||
|
// as their submission is observed complete.
|
||||||
VkResult RetireCurrentCommandBuffer();
|
VkResult RetireCurrentCommandBuffer();
|
||||||
|
|
||||||
|
// Frees every retired command buffer whose tagged submission index is
|
||||||
|
// known complete. Driven by the renderer's submit tracker on completion
|
||||||
|
// events (fence waits and non-blocking polls), so present-less flush
|
||||||
|
// loops reclaim their buffers without any extra wait.
|
||||||
|
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
|
||||||
|
// Frees every slot's retired command buffers. Only valid when the
|
||||||
|
// caller has proven every queue submission complete.
|
||||||
|
void FreeAllRetiredCommandBuffers();
|
||||||
|
|
||||||
Uint32 GetCurrentFrameIndex() const;
|
Uint32 GetCurrentFrameIndex() const;
|
||||||
Uint32 GetFrameCount() const;
|
Uint32 GetFrameCount() const;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "PipelineFactory.h"
|
#include "PipelineFactory.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||||
@@ -243,23 +244,108 @@ 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::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 (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.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::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
|
||||||
|
evicted, sortedPasses.size());
|
||||||
|
}
|
||||||
|
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 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
|
||||||
|
// 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
|
||||||
|
|||||||
@@ -211,6 +211,40 @@ 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()) {
|
||||||
|
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: freed %zu descriptor sets for destroyed layout", 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,
|
||||||
@@ -911,6 +945,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkDescriptorPoolCreateInfo poolInfo{};
|
VkDescriptorPoolCreateInfo poolInfo{};
|
||||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
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.maxSets = maxSets;
|
||||||
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
||||||
poolInfo.pPoolSizes = poolSizes;
|
poolInfo.pPoolSizes = poolSizes;
|
||||||
@@ -990,7 +1029,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& frame = m_frames[frameIndex];
|
auto& frame = m_frames[frameIndex];
|
||||||
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
|
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
|
||||||
if (cache.cursor < cache.sets.size()) {
|
if (cache.cursor < cache.sets.size()) {
|
||||||
outDescriptorSet = cache.sets[cache.cursor++];
|
outDescriptorSet = cache.sets[cache.cursor++].set;
|
||||||
} else {
|
} else {
|
||||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||||
@@ -1004,7 +1043,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return allocResult;
|
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;
|
++cache.cursor;
|
||||||
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
|
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
|
||||||
cache.sets.size());
|
cache.sets.size());
|
||||||
|
|||||||
@@ -39,6 +39,17 @@ 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 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);
|
||||||
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);
|
||||||
@@ -65,8 +76,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 allocatedSets = 0;
|
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 {
|
struct DescriptorSetCacheEntry {
|
||||||
Vector<VkDescriptorSet> sets;
|
Vector<CachedDescriptorSet> sets;
|
||||||
Uint32 cursor = 0;
|
Uint32 cursor = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -141,6 +141,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_transientUploadArena.BeginFrame(frameIndex);
|
m_transientUploadArena.BeginFrame(frameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VkBufferManager::CollectAllDeferredReleases() {
|
||||||
|
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
||||||
|
CollectDeferredReleases(frameIndex);
|
||||||
|
}
|
||||||
|
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
|
||||||
|
m_transientUploadArena.CollectDeferredReleases(frameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void VkBufferManager::NotifyDeviceIdle() {
|
void VkBufferManager::NotifyDeviceIdle() {
|
||||||
// Everything submitted so far has completed. Work recorded for the
|
// Everything submitted so far has completed. Work recorded for the
|
||||||
// current frame has not been submitted yet, so the current serial
|
// current frame has not been submitted yet, so the current serial
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Recreate all per-frame transient arenas
|
// Recreate all per-frame transient arenas
|
||||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
|
// Drains every frame slot's deferred buffer/resource releases (and the
|
||||||
|
// transient arena's parked superseded blocks). Only valid when the
|
||||||
|
// caller has proven every queue submission complete; used by the
|
||||||
|
// present-less frame-boundary drain.
|
||||||
|
void CollectAllDeferredReleases();
|
||||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||||
void NotifyDeviceIdle();
|
void NotifyDeviceIdle();
|
||||||
// A frame slot's submission fence has been waited: every serial up to
|
// A frame slot's submission fence has been waited: every serial up to
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
internalFormat = TextureInternalFormat::Unknown;
|
internalFormat = TextureInternalFormat::Unknown;
|
||||||
samples = 0;
|
samples = 0;
|
||||||
|
deadSinceFrame = kNeverObservedDead;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkRenderPassManager::VkRenderPassManager(VkDevice device,
|
VkRenderPassManager::VkRenderPassManager(VkDevice device,
|
||||||
@@ -206,6 +207,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.Destroy(m_device, m_allocator);
|
resource.Destroy(m_device, m_allocator);
|
||||||
}
|
}
|
||||||
m_renderbufferResources.clear();
|
m_renderbufferResources.clear();
|
||||||
|
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
|
||||||
m_pendingRenderbufferClears.clear();
|
m_pendingRenderbufferClears.clear();
|
||||||
RenderPassEntry::s_textureResourcesScratch.clear();
|
RenderPassEntry::s_textureResourcesScratch.clear();
|
||||||
s_activeRenderPass = {};
|
s_activeRenderPass = {};
|
||||||
@@ -213,22 +215,75 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_rpFastValid = false;
|
m_rpFastValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkRenderPassManager::CollectRenderbufferGarbage() {
|
Uint64 VkRenderPassManager::RetireAgeFrames() const {
|
||||||
Vector<MG_State::GLState::RenderbufferObject*> deadRenderbuffers;
|
// MaxFramesInFlight + 2 covers the frame ring plus one boundary for the
|
||||||
deadRenderbuffers.reserve(m_renderbufferResources.size());
|
// recording-to-submit gap and one because OnPresent runs ahead of Present's
|
||||||
for (auto& [renderbuffer, resource] : m_renderbufferResources) {
|
// fence wait; the floor of 8 keeps a margin over the default ring of 3 while
|
||||||
const auto liveRenderbuffer = resource.renderbuffer.lock();
|
// still releasing multi-MB attachment memory promptly (the render-pass cache's
|
||||||
if (!liveRenderbuffer || liveRenderbuffer.get() != renderbuffer) {
|
// 1024-frame retirement would pin it for no additional safety).
|
||||||
deadRenderbuffers.emplace_back(renderbuffer);
|
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;
|
||||||
}
|
}
|
||||||
for (auto* renderbuffer : deadRenderbuffers) {
|
m_deferredRenderbufferReleases.push_back({resource.image, resource.allocation, resource.view, m_frameCounter});
|
||||||
auto resourceIt = m_renderbufferResources.find(renderbuffer);
|
resource.image = VK_NULL_HANDLE;
|
||||||
if (resourceIt != m_renderbufferResources.end()) {
|
resource.allocation = nullptr;
|
||||||
resourceIt->second.Destroy(m_device, m_allocator);
|
resource.view = VK_NULL_HANDLE;
|
||||||
m_renderbufferResources.erase(resourceIt);
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
m_pendingRenderbufferClears.erase(renderbuffer);
|
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 (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();
|
||||||
|
if (liveRenderbuffer && liveRenderbuffer.get() == it->first) {
|
||||||
|
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resource.deadSinceFrame == RenderbufferResource::kNeverObservedDead) {
|
||||||
|
resource.deadSinceFrame = m_frameCounter;
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (m_frameCounter - resource.deadSinceFrame < retireAgeFrames) {
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_pendingRenderbufferClears.erase(it->first);
|
||||||
|
resource.Destroy(m_device, m_allocator);
|
||||||
|
it = m_renderbufferResources.erase(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,9 +325,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.samples != renderbuffer->GetSamples();
|
resource.samples != renderbuffer->GetSamples();
|
||||||
if (!needsCreate) {
|
if (!needsCreate) {
|
||||||
resource.renderbuffer = renderbuffer;
|
resource.renderbuffer = renderbuffer;
|
||||||
|
// A new renderbuffer at a recycled address may adopt a compatible entry that
|
||||||
|
// was already stamped dead; it is alive again, so cancel the aging.
|
||||||
|
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
||||||
return &resource;
|
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.Destroy(m_device, m_allocator);
|
||||||
resource.renderbuffer = renderbuffer;
|
resource.renderbuffer = renderbuffer;
|
||||||
|
|
||||||
@@ -1178,6 +1239,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkRenderPassManager::OnPresent() {
|
void VkRenderPassManager::OnPresent() {
|
||||||
++m_frameCounter;
|
++m_frameCounter;
|
||||||
|
|
||||||
|
// Runs every frame boundary, ahead of the render-pass sweep gate below: the walk
|
||||||
|
// is O(#renderbuffer resources) — single digits in practice — and per-frame
|
||||||
|
// invocation keeps dead-resource reclaim latency at the aging bound instead of
|
||||||
|
// 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
|
// Sweep occasionally; evict entries whose last use is far past every
|
||||||
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
||||||
// safely (RenderPassEntry's destructor releases the handles).
|
// safely (RenderPassEntry's destructor releases the handles).
|
||||||
@@ -1187,6 +1256,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return;
|
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;
|
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
||||||
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
||||||
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
||||||
@@ -1194,11 +1269,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
destroyedRenderPasses.push_back(it->second.renderPass);
|
||||||
it = m_renderPasses.erase(it);
|
it = m_renderPasses.erase(it);
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
|
||||||
|
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
||||||
|
|||||||
@@ -157,11 +157,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
class VkRenderPassManager {
|
class VkRenderPassManager {
|
||||||
public:
|
public:
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
|
|
||||||
|
// 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 OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 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 +212,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
|
||||||
@@ -213,6 +234,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
struct RenderbufferResource {
|
struct RenderbufferResource {
|
||||||
|
// deadSinceFrame sentinel: the owning weak reference has not been observed
|
||||||
|
// expired. Dead resources age past every in-flight frame before Destroy
|
||||||
|
// (see CollectRenderbufferGarbage); the GPU may still reference the image
|
||||||
|
// for frames-in-flight frames after the GL object dies.
|
||||||
|
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
|
||||||
|
|
||||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||||
VkImage image = VK_NULL_HANDLE;
|
VkImage image = VK_NULL_HANDLE;
|
||||||
VmaAllocation allocation = nullptr;
|
VmaAllocation allocation = nullptr;
|
||||||
@@ -224,6 +251,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||||
Int samples = 0;
|
Int samples = 0;
|
||||||
|
// m_frameCounter value at which the weak reference was first seen expired.
|
||||||
|
Uint64 deadSinceFrame = kNeverObservedDead;
|
||||||
|
|
||||||
void Destroy(VkDevice device, VmaAllocator allocator);
|
void Destroy(VkDevice device, VmaAllocator allocator);
|
||||||
};
|
};
|
||||||
@@ -241,12 +270,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ClearAttachmentPayload payload{};
|
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*, RenderbufferResource> m_renderbufferResources;
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||||
|
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||||
|
|
||||||
Bool HasPendingRenderbufferClear(
|
Bool HasPendingRenderbufferClear(
|
||||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||||
void CollectRenderbufferGarbage();
|
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 XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -627,6 +627,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
frameIndex, m_deferredViewReleases.size());
|
frameIndex, m_deferredViewReleases.size());
|
||||||
m_currentFrameIndex = frameIndex;
|
m_currentFrameIndex = frameIndex;
|
||||||
CollectDeferredReleases(frameIndex);
|
CollectDeferredReleases(frameIndex);
|
||||||
|
|
||||||
|
// Frame-boundary GC: every 64 frame boundaries (~1 s at 60 fps) bounds the reclaim
|
||||||
|
// latency for dead textures regardless of draw traffic — workloads that churn
|
||||||
|
// textures through clears/readbacks alone never reach the draw-gated
|
||||||
|
// CollectGarbage. Must run after CollectDeferredReleases above: the prune defers
|
||||||
|
// its releases into this frame's slot, which was just drained, so they are
|
||||||
|
// destroyed only after the slot's fence has been waited again one full frame-ring
|
||||||
|
// cycle from now (never while an in-flight frame may still reference them).
|
||||||
|
constexpr Uint32 kGcFrameInterval = 64;
|
||||||
|
++m_gcFrameCounter;
|
||||||
|
if (m_gcFrameCounter % kGcFrameInterval == 0) {
|
||||||
|
PruneDeadTextures();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkTextureManager::CollectAllDeferredReleases() {
|
||||||
|
const SizeT frameCount = std::min(m_deferredReleases.size(), m_deferredViewReleases.size());
|
||||||
|
for (SizeT frameIndex = 0; frameIndex < frameCount; ++frameIndex) {
|
||||||
|
CollectDeferredReleases(static_cast<Uint32>(frameIndex));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
|
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
|
||||||
@@ -702,9 +722,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// construction introduces a new identity. Doing this unconditionally made every
|
// construction introduces a new identity. Doing this unconditionally made every
|
||||||
// sampled-texture sync scan the entire alive-texture map per draw.
|
// sampled-texture sync scan the entire alive-texture map per draw.
|
||||||
if (aliveIt == m_aliveObjects.end()) {
|
if (aliveIt == m_aliveObjects.end()) {
|
||||||
|
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
|
||||||
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
||||||
if (liveTexture && liveTexture.get() == &texture) {
|
if (liveTexture && liveTexture.get() == &texture) {
|
||||||
m_aliveObjects[identity] = WeakPtr<MG_State::GLState::ITextureObject>(liveTexture);
|
aliveTexture = liveTexture;
|
||||||
|
} else {
|
||||||
|
// The name lookup legally fails while the object is alive: the name was
|
||||||
|
// deleted with the texture still attached to an FBO (the attachment's
|
||||||
|
// SharedPtr keeps it alive), or the name was reused by a new texture, or
|
||||||
|
// this is a default texture object (name 0 lives outside the name map).
|
||||||
|
// Register through the object's own control block so the resource created
|
||||||
|
// below still participates in weak-expiry GC instead of becoming an
|
||||||
|
// orphan no reclamation path can reach until Shutdown.
|
||||||
|
aliveTexture = texture.weak_from_this();
|
||||||
|
}
|
||||||
|
if (!aliveTexture.expired()) {
|
||||||
|
m_aliveObjects[identity] = Move(aliveTexture);
|
||||||
PruneStaleTextureAliases(&texture);
|
PruneStaleTextureAliases(&texture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1212,10 +1245,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SizeT VkTextureManager::CollectGarbage() {
|
SizeT VkTextureManager::CollectGarbage() {
|
||||||
|
// Draw-gated stagger (1 in 256 calls): keeps the per-draw cost at one counter
|
||||||
|
// bump. The guaranteed reclaim path is the frame-boundary prune in BeginFrame;
|
||||||
|
// this remains as a cheap assist so draw-heavy workloads reclaim sooner.
|
||||||
m_gcCounter++;
|
m_gcCounter++;
|
||||||
if (m_gcCounter != 0) {
|
if (m_gcCounter != 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
return PruneDeadTextures();
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT VkTextureManager::PruneDeadTextures() {
|
||||||
|
// Erasing entries would dangle the raw TextureResource pointers memoized for the
|
||||||
|
// current draw; every call path (BeginFrame, and CollectGarbage at the top of a
|
||||||
|
// freshly opened draw-sync scope) runs before any memo entry is recorded.
|
||||||
|
MOBILEGL_ASSERT(m_drawSyncedThisDraw.empty(),
|
||||||
|
"PruneDeadTextures: draw-sync memo holds raw resource pointers an erase would dangle");
|
||||||
|
|
||||||
Vector<MG_State::GLState::ITextureObject*> expiredTextures;
|
Vector<MG_State::GLState::ITextureObject*> expiredTextures;
|
||||||
expiredTextures.reserve(m_aliveObjects.size());
|
expiredTextures.reserve(m_aliveObjects.size());
|
||||||
@@ -1227,7 +1272,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto* texture : expiredTextures) {
|
for (auto* texture : expiredTextures) {
|
||||||
PruneStaleTextureAliases(texture);
|
PruneStaleTextureAliases(texture);
|
||||||
}
|
}
|
||||||
return expiredTextures.size();
|
SizeT prunedCount = expiredTextures.size();
|
||||||
|
|
||||||
|
// Orphan sweep: after the pass above, m_aliveObjects holds only live entries.
|
||||||
|
// Registration in SyncTextureAndGetDescriptor cannot fail for a SharedPtr-owned
|
||||||
|
// texture (weak_from_this fallback), so a resource whose identity has no alive
|
||||||
|
// entry has no trackable owner: its GL-side object is gone, or was never
|
||||||
|
// shared-owned, in which case recreation on a later sync is the safe fallback.
|
||||||
|
// Destruction goes through the per-frame deferred queues, never immediate.
|
||||||
|
Vector<TextureIdentity> orphanIdentities;
|
||||||
|
for (auto it = m_textureResources.begin(); it != m_textureResources.end(); ++it) {
|
||||||
|
if (m_aliveObjects.find(it->first) == m_aliveObjects.end()) {
|
||||||
|
orphanIdentities.emplace_back(it->first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& identity : orphanIdentities) {
|
||||||
|
EraseTrackedTexture(identity);
|
||||||
|
}
|
||||||
|
prunedCount += orphanIdentities.size();
|
||||||
|
return prunedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||||
|
|||||||
@@ -267,6 +267,10 @@ public:
|
|||||||
Bool Initialize(const InitInfo& initInfo);
|
Bool Initialize(const InitInfo& initInfo);
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
|
// Drains every frame slot's deferred image/view releases. Only valid when
|
||||||
|
// the caller has proven every queue submission complete; used by the
|
||||||
|
// present-less frame-boundary drain.
|
||||||
|
void CollectAllDeferredReleases();
|
||||||
|
|
||||||
TextureResource* SyncTextureAndGetDescriptor(
|
TextureResource* SyncTextureAndGetDescriptor(
|
||||||
MG_State::GLState::ITextureObject& texture);
|
MG_State::GLState::ITextureObject& texture);
|
||||||
@@ -364,6 +368,7 @@ private:
|
|||||||
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||||
|
SizeT PruneDeadTextures();
|
||||||
|
|
||||||
VkDevice m_device = VK_NULL_HANDLE;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
@@ -373,6 +378,9 @@ private:
|
|||||||
Uint32 m_currentFrameIndex = 0;
|
Uint32 m_currentFrameIndex = 0;
|
||||||
|
|
||||||
Uint8 m_gcCounter = 0;
|
Uint8 m_gcCounter = 0;
|
||||||
|
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
|
||||||
|
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
|
||||||
|
Uint32 m_gcFrameCounter = 0;
|
||||||
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
||||||
// textures already fully synced in the current draw (small N -> flat scan).
|
// textures already fully synced in the current draw (small N -> flat scan).
|
||||||
Bool m_drawSyncScopeActive = false;
|
Bool m_drawSyncScopeActive = false;
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -6204,12 +6218,12 @@ void main() {
|
|||||||
|
|
||||||
frame.hasCommandBufferRecorded = false;
|
frame.hasCommandBufferRecorded = false;
|
||||||
frame.isCommandRecording = false;
|
frame.isCommandRecording = false;
|
||||||
// The wait proved every descriptor set this slot has in flight idle;
|
// The wait proved every submission complete, so the full frame-boundary
|
||||||
// rewind the reuse cursors so present-less readback loops stay
|
// drain applies: descriptor cursors, transient arenas, deferred
|
||||||
// bounded (Present is the only other rewind point).
|
// texture/buffer releases, retired command buffers and the converted
|
||||||
if (m_uniformManager) {
|
// vertex-stream cache all rewind here, keeping present-less readback
|
||||||
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
// loops bounded (Present is the only other drain point).
|
||||||
}
|
TryDrainFrameTransients();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7123,6 +7137,9 @@ void main() {
|
|||||||
}
|
}
|
||||||
m_bufferManager.NotifyDeviceIdle();
|
m_bufferManager.NotifyDeviceIdle();
|
||||||
OnSubmitsCompletedUpTo(m_submitCounter);
|
OnSubmitsCompletedUpTo(m_submitCounter);
|
||||||
|
// The queue was just drained; take the free frame-boundary drain when
|
||||||
|
// nothing is recorded (present-less timer-query loops). No-op otherwise.
|
||||||
|
TryDrainFrameTransients();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7191,6 +7208,85 @@ void main() {
|
|||||||
vkDestroyFence(m_device, record.fence, nullptr);
|
vkDestroyFence(m_device, record.fence, nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Mid-frame-flushed command buffers whose submission just completed can
|
||||||
|
// be freed now; present-less flush loops have no other reclaim point.
|
||||||
|
m_frameContext.FreeRetiredCommandBuffersCompletedUpTo(m_completedSubmitCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VulkanRenderer::TryDrainFrameTransients() {
|
||||||
|
if (m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_completedSubmitCounter != m_submitCounter) {
|
||||||
|
RefreshCompletedSubmits();
|
||||||
|
if (m_completedSubmitCounter != m_submitCounter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (HasPendingRecordedWork()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every submission is complete and nothing recorded references the
|
||||||
|
// 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();
|
||||||
|
m_frameContext.FreeAllRetiredCommandBuffers();
|
||||||
|
for (Uint32 slot = 0; slot < m_deferredDepthMipmapCleanup.size(); ++slot) {
|
||||||
|
CollectDeferredDepthMipmapCleanup(slot);
|
||||||
|
}
|
||||||
|
if (m_textureManager) {
|
||||||
|
m_textureManager->CollectAllDeferredReleases();
|
||||||
|
}
|
||||||
|
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_renderPassManager) {
|
||||||
|
m_renderPassManager->OnPresent();
|
||||||
|
}
|
||||||
|
// 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_vertexInputStateFactory) {
|
||||||
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
|
}
|
||||||
|
if (m_samplerManager) {
|
||||||
|
m_samplerManager->OnFrameBoundary();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkFence VulkanRenderer::AcquirePooledSubmitFence() {
|
VkFence VulkanRenderer::AcquirePooledSubmitFence() {
|
||||||
@@ -7253,6 +7349,10 @@ void main() {
|
|||||||
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
|
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Non-blocking completion poll: gives flush-only workloads (no sync
|
||||||
|
// objects, no present) a point where finished submissions retire their
|
||||||
|
// pooled fences and mid-frame command buffers.
|
||||||
|
RefreshCompletedSubmits();
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
if (!frame.isCommandRecording && !frame.hasCommandBufferRecorded) {
|
if (!frame.isCommandRecording && !frame.hasCommandBufferRecorded) {
|
||||||
return false;
|
return false;
|
||||||
@@ -7277,6 +7377,14 @@ void main() {
|
|||||||
return false;
|
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
|
// The submitted command buffer may still be executing; recording must
|
||||||
// restart on a fresh one. If none can be allocated, fall back to
|
// restart on a fresh one. If none can be allocated, fall back to
|
||||||
// draining this submission so reusing the buffer stays legal.
|
// draining this submission so reusing the buffer stays legal.
|
||||||
@@ -7328,6 +7436,10 @@ void main() {
|
|||||||
const VkResult result = vkWaitForFences(m_device, 1, &record.fence, VK_TRUE, timeoutNs);
|
const VkResult result = vkWaitForFences(m_device, 1, &record.fence, VK_TRUE, timeoutNs);
|
||||||
if (result == VK_SUCCESS) {
|
if (result == VK_SUCCESS) {
|
||||||
OnSubmitsCompletedUpTo(record.submitIndex);
|
OnSubmitsCompletedUpTo(record.submitIndex);
|
||||||
|
// The wait already stalled the pipeline; if it happens to
|
||||||
|
// have drained everything (present-less fence loops), take
|
||||||
|
// the free frame-boundary drain. No-op otherwise.
|
||||||
|
TryDrainFrameTransients();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result != VK_TIMEOUT) {
|
if (result != VK_TIMEOUT) {
|
||||||
@@ -7417,6 +7529,13 @@ void main() {
|
|||||||
suspendedFrame.isCommandRecording = false;
|
suspendedFrame.isCommandRecording = false;
|
||||||
suspendedFrame.hasCommandBufferRecorded = false;
|
suspendedFrame.hasCommandBufferRecorded = false;
|
||||||
m_lastPipelineValid = false;
|
m_lastPipelineValid = false;
|
||||||
|
// The dropped recording is never submitted, so once the fence
|
||||||
|
// poll shows the pre-suspension submissions complete the frame
|
||||||
|
// transients (descriptor sets, transient arenas, deferred
|
||||||
|
// releases, conversion caches) can rewind; without this a
|
||||||
|
// minimized-window app accumulates them for the whole
|
||||||
|
// suspension.
|
||||||
|
TryDrainFrameTransients();
|
||||||
MGLOG_D("Present skipped: no usable swapchain (zero-area window)");
|
MGLOG_D("Present skipped: no usable swapchain (zero-area window)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -7430,6 +7549,22 @@ 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();
|
||||||
|
// 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 (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_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)
|
||||||
@@ -8462,6 +8597,43 @@ void main() {
|
|||||||
m_computePipelines.clear();
|
m_computePipelines.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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->EvictByRenderPasses(renderPasses) > 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 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 OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) 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);
|
||||||
@@ -345,11 +362,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkFence AcquirePooledSubmitFence();
|
VkFence AcquirePooledSubmitFence();
|
||||||
void DestroySubmitFencePool();
|
void DestroySubmitFencePool();
|
||||||
Bool HasPendingRecordedWork() const;
|
Bool HasPendingRecordedWork() const;
|
||||||
|
// Frame-boundary housekeeping for paths that never reach Present's
|
||||||
|
// tail (present-less readback loops, suspended presentation, blocking
|
||||||
|
// sync waits): runs the same per-frame drains Present performs, but
|
||||||
|
// only when every queue submission has been observed complete AND no
|
||||||
|
// recorded-but-unsubmitted commands exist - i.e. when CPU-GPU overlap
|
||||||
|
// is provably already zero. Never blocks (non-blocking fence poll
|
||||||
|
// only), so the presenting path's frames-in-flight pipelining is
|
||||||
|
// untouched. Returns true when the drain ran.
|
||||||
|
Bool TryDrainFrameTransients();
|
||||||
|
|
||||||
Vector<SubmitRecord> m_inFlightSubmits;
|
Vector<SubmitRecord> m_inFlightSubmits;
|
||||||
Vector<VkFence> m_freeSubmitFences;
|
Vector<VkFence> m_freeSubmitFences;
|
||||||
Uint64 m_submitCounter = 0;
|
Uint64 m_submitCounter = 0;
|
||||||
Uint64 m_completedSubmitCounter = 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;
|
NativeWindowType m_window = 0;
|
||||||
void* m_platformDisplay = nullptr;
|
void* m_platformDisplay = nullptr;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -15,7 +15,11 @@
|
|||||||
#include <MG_Util/Math/VectorTypes.h>
|
#include <MG_Util/Math/VectorTypes.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
class ITextureObject {
|
// Texture objects are always SharedPtr-owned (TextureState creates every instance via
|
||||||
|
// MakeShared, including the per-target default objects). enable_shared_from_this lets
|
||||||
|
// backends that only receive a reference (e.g. syncing a name-deleted texture kept
|
||||||
|
// alive by an FBO attachment) still register a weak liveness reference for GC.
|
||||||
|
class ITextureObject : public std::enable_shared_from_this<ITextureObject> {
|
||||||
public:
|
public:
|
||||||
using TargetEnum = TextureTarget;
|
using TargetEnum = TextureTarget;
|
||||||
virtual ~ITextureObject() = default;
|
virtual ~ITextureObject() = default;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
#include <FastSTL/UnorderedMap.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
|
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
|
||||||
@@ -1736,3 +1737,61 @@ TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
|||||||
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7); // must reach the driver again
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7); // must reach the driver again
|
||||||
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 3u);
|
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 3u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FastSTL::unordered_map::erase(iterator) regression coverage. The open-addressing
|
||||||
|
// iterator constructor snaps forward from a tombstoned slot to the successor, so
|
||||||
|
// erase must NOT advance the rebuilt iterator again: the old double-advance skipped
|
||||||
|
// one live element per erase, and erasing the element in the highest occupied
|
||||||
|
// bucket pushed the returned index past bucket_count where it never compared equal
|
||||||
|
// to end() again - erase-while-iterating sweeps (pipeline/program cache eviction)
|
||||||
|
// then ran off the bucket array and fed garbage handles to vkDestroyPipeline
|
||||||
|
// (device crash on first mass eviction during world load).
|
||||||
|
TEST(FastSTLSanity, EraseWhileIteratingVisitsEveryElementExactlyOnce) {
|
||||||
|
FastSTL::unordered_map<MobileGL::Uint64, MobileGL::Uint64> map;
|
||||||
|
constexpr MobileGL::Uint64 kCount = 1000;
|
||||||
|
for (MobileGL::Uint64 key = 0; key < kCount; ++key) {
|
||||||
|
map.emplace(key * 0x9e3779b97f4a7c15ull, key);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(map.size(), kCount);
|
||||||
|
|
||||||
|
MobileGL::SizeT visited = 0;
|
||||||
|
for (auto it = map.begin(); it != map.end();) {
|
||||||
|
it = map.erase(it);
|
||||||
|
++visited;
|
||||||
|
ASSERT_LE(visited, kCount); // old code: runaway past end / skipped entries
|
||||||
|
}
|
||||||
|
EXPECT_EQ(visited, kCount);
|
||||||
|
EXPECT_EQ(map.size(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FastSTLSanity, EraseReturnsTheSuccessorElement) {
|
||||||
|
FastSTL::unordered_map<MobileGL::Uint32, MobileGL::Uint32> map;
|
||||||
|
for (MobileGL::Uint32 key = 1; key <= 64; ++key) {
|
||||||
|
map.emplace(key, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erasing every other visited element must still visit all 64 exactly once:
|
||||||
|
// the iterator returned by erase names the very next element, not one past it.
|
||||||
|
MobileGL::SizeT visited = 0;
|
||||||
|
MobileGL::SizeT erased = 0;
|
||||||
|
for (auto it = map.begin(); it != map.end();) {
|
||||||
|
++visited;
|
||||||
|
if ((visited & 1) != 0) {
|
||||||
|
it = map.erase(it);
|
||||||
|
++erased;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
ASSERT_LE(visited, 64u);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(visited, 64u);
|
||||||
|
EXPECT_EQ(map.size(), 64u - erased);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FastSTLSanity, ErasingTheOnlyElementReturnsEnd) {
|
||||||
|
FastSTL::unordered_map<MobileGL::Uint32, MobileGL::Uint32> map;
|
||||||
|
map.emplace(42u, 1u);
|
||||||
|
auto next = map.erase(map.begin());
|
||||||
|
EXPECT_EQ(next, map.end());
|
||||||
|
EXPECT_TRUE(map.empty());
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
Submodule include/FastSTL updated: 34f55f9df2...022211c998
Reference in New Issue
Block a user