mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix] (DirectVulkan): drain frame transients on present-less paths - readback waits, suspended presentation, blocking sync waits and flush completion polls now run Present's per-frame drains (deferred buffer/texture releases, transient arena rewind, descriptor cursors, retired command buffers, conversion caches) whenever every submission is provably complete, so offscreen/minimized workloads stay bounded; never blocks, frames-in-flight overlap untouched
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -629,6 +629,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
CollectDeferredReleases(frameIndex);
|
CollectDeferredReleases(frameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
auto resourceIt = m_textureResources.find(identity);
|
auto resourceIt = m_textureResources.find(identity);
|
||||||
if (resourceIt != m_textureResources.end()) {
|
if (resourceIt != m_textureResources.end()) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -6204,12 +6204,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 +7123,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 +7194,54 @@ 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, so the drains Present's tail performs are safe
|
||||||
|
// here too. Raise the buffer manager's completed floor first: the
|
||||||
|
// serial inference (frameSerial - frameCount) is only justified by
|
||||||
|
// Present's slot-fence cadence, and the extra BeginFrame below would
|
||||||
|
// otherwise inflate it past reality.
|
||||||
|
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_textureManager->BeginFrame(frameIndex);
|
||||||
|
}
|
||||||
|
m_bufferManager.CollectAllDeferredReleases();
|
||||||
|
m_bufferManager.BeginFrame(frameIndex);
|
||||||
|
// The cached conversion slices point into the transient arena the
|
||||||
|
// BeginFrame above just rewound; drop them together.
|
||||||
|
m_convertedVertexStreams.clear();
|
||||||
|
if (m_uniformManager) {
|
||||||
|
m_uniformManager->BeginFrame(frameIndex);
|
||||||
|
}
|
||||||
|
if (m_renderPassManager) {
|
||||||
|
m_renderPassManager->OnPresent();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkFence VulkanRenderer::AcquirePooledSubmitFence() {
|
VkFence VulkanRenderer::AcquirePooledSubmitFence() {
|
||||||
@@ -7253,6 +7304,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;
|
||||||
@@ -7328,6 +7383,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 +7476,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -345,6 +345,15 @@ 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user