From 7ab83861ca0a6744e61193dcc245b136b2445471 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 07:59:40 -0400 Subject: [PATCH] [Fix] (DirectVulkan): suspend presentation while the window is zero-area - a minimized window's out-of-date swapchain used to keep Present submitting on a signaled fence and presenting never-acquired images (adversarial review); also drop logging from the process-detach abandon path --- MobileGL/Init.cpp | 5 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 58 ++++++++++++++----- .../DirectVulkan/Renderer/VulkanRenderer.h | 8 ++- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/MobileGL/Init.cpp b/MobileGL/Init.cpp index 2d08bce4..2f711d5d 100644 --- a/MobileGL/Init.cpp +++ b/MobileGL/Init.cpp @@ -95,15 +95,16 @@ namespace MobileGL { // inside a dying process - other threads have already been terminated, // so Vulkan/GLES cleanup crashes with the process half-dead. The process // is exiting; the OS reclaims everything. + // No logging here: this runs under the loader lock after every other + // thread was terminated, where the log mutex/heap/stdio may be in any + // state. Plain pointer stores only. void AbandonAtProcessExit() { - MGLOG_I("MobileGL: process detach, abandoning global state"); MG_Backend::DirectVulkan::pVulkanRenderer.release(); MG_Backend::pActiveBackendObject.release(); MG_State::pGLContext.release(); MG_State::pEGLContext.release(); MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.release(); MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.release(); - MGLOG_I("MobileGL: global state abandoned"); } } // namespace #endif diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e7566207..a0ec851d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -6865,18 +6865,32 @@ void main() { } void VulkanRenderer::Present() { - if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE) { - // No swapchain yet (the window had a zero-area surface at initialization). - // Try to bring one up now that the window may have a real size; if it is - // still zero-area there is nothing to present to. - RecreateSwapchain(); - if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE) { - MGLOG_D("Present skipped: still no swapchain (zero-area window)"); + if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended) { + // No usable swapchain: the window was zero-area at initialization, or + // presentation was suspended when the window minimized. Try to bring a + // swapchain up now that the window may have a real size; until then, drop + // this frame's recording instead of submitting - a submit would wait on a + // never-signaled acquire semaphore and reuse a still-signaled fence. + if (!RecreateSwapchain()) { + auto& suspendedFrame = m_frameContext.GetCurrent(); + if (VkRenderPassManager::GetActiveRenderPass()) { + VkRenderPassManager::EndRenderPass(suspendedFrame.commandBuffer); + } + if (suspendedFrame.isCommandRecording) { + m_frameContext.EndCommandRecording(); + } + suspendedFrame.isCommandRecording = false; + suspendedFrame.hasCommandBufferRecorded = false; + m_lastPipelineValid = false; + MGLOG_D("Present skipped: no usable swapchain (zero-area window)"); return; } + m_presentSuspended = false; const VkResult acquireResult = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); - VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage"); + if (acquireResult != VK_SUBOPTIMAL_KHR) { + VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage"); + } } MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(), "Present, acquired image index out of range"); @@ -6911,14 +6925,26 @@ void main() { auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result); - RecreateSwapchain(); + if (!RecreateSwapchain()) { + // Window went zero-area (minimize) with the swapchain out of date: + // stop submitting/acquiring until it has a size again. + m_presentSuspended = true; + m_swapchainResizeRequested = false; + MGLOG_D("Present, zero-area window with out-of-date swapchain; suspending presentation"); + return; + } m_swapchainResizeRequested = false; result = VK_SUCCESS; } VK_VERIFY(result, "Present, vkQueuePresentKHR"); if (m_swapchainResizeRequested) { MGLOG_D("Present, processing requested swapchain resize"); - RecreateSwapchain(); + if (!RecreateSwapchain()) { + m_presentSuspended = true; + m_swapchainResizeRequested = false; + MGLOG_D("Present, zero-area window on requested resize; suspending presentation"); + return; + } m_swapchainResizeRequested = false; } @@ -6929,7 +6955,12 @@ void main() { result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result); - RecreateSwapchain(); + if (!RecreateSwapchain()) { + m_presentSuspended = true; + m_swapchainResizeRequested = false; + MGLOG_D("Present, zero-area window on next-frame acquire; suspending presentation"); + return; + } m_swapchainResizeRequested = false; result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); @@ -7743,13 +7774,13 @@ void main() { m_swapchainObject.Shutdown(m_device); } - void VulkanRenderer::RecreateSwapchain() { + Bool VulkanRenderer::RecreateSwapchain() { // Handle cases like minimize on Windows, where swapchain could return a 0x0 extent const auto swapchainCapabilities = SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface); if (swapchainCapabilities.capabilities.currentExtent.width == 0 || swapchainCapabilities.capabilities.currentExtent.height == 0) { - return; + return false; } vkDeviceWaitIdle(m_device); @@ -7793,6 +7824,7 @@ void main() { m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_convertedVertexStreams.clear(); } + return true; } const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index ee3cd507..d691aff8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -257,7 +257,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const; void RequestSwapchainResize(Uint32 width, Uint32 height); - void RecreateSwapchain(); + // Returns false when the surface is zero-area (minimized/hidden window): + // no new swapchain is installed and presentation must stay suspended. + Bool RecreateSwapchain(); private: struct BlitUniformData { @@ -355,6 +357,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void* m_platformCloseDisplay = nullptr; VulkanRendererConfig m_config; Bool m_swapchainResizeRequested = false; + // Presentation is suspended while the window is zero-area (minimized): the + // swapchain is unusable/out of date, so Present drops frames instead of + // submitting on a signaled fence / presenting never-acquired images. + Bool m_presentSuspended = false; // Vulkan objects Bool m_validationLayersEnabled = false;