From 0ea9e6de5f6c023110062eab14a33a81349e568c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 28 Jul 2026 21:02:09 -0400 Subject: [PATCH] [Fix] (DirectVulkan): follow surface resizes instead of rebuilding the swapchain on VK_SUBOPTIMAL_KHR - a per-frame surface-capabilities comparison (ANGLE's model) is now the only thing that schedules a rebuild, so a launcher-side resolution change reaches the swapchain and the compositor scales the smaller image up to the view, while a driver that merely reports the surface as suboptimal can no longer rebuild every frame (each rebuild destroys every pipeline, resets the render-pass manager and reallocates the default framebuffer, which showed as flicker, then corruption, then a crash); the comparison runs in SURFACE space against the extent the live swapchain was created from, since comparing against the swapchain's own quarter-turn-swapped extent reports a difference on every rotated frame --- .../DirectVulkan/Renderer/SwapchainObject.cpp | 5 ++ .../DirectVulkan/Renderer/SwapchainObject.h | 4 + .../DirectVulkan/Renderer/VulkanRenderer.cpp | 78 +++++++++++++++---- .../DirectVulkan/Renderer/VulkanRenderer.h | 4 + 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp index e58c079b..2ba3c10c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp @@ -247,6 +247,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace}; m_extent = createInfo.imageExtent; + // The surface-space extent this swapchain was built from, i.e. before the + // quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a + // freshly queried currentExtent against the swapped m_extent flips axes every rotation + // and makes the comparison alternate forever. + m_surfaceExtent = defaultFramebufferExtent; m_preTransform = createInfo.preTransform; VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain)); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h index 663a5f52..968027a6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h @@ -35,6 +35,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkSwapchainKHR GetHandle() const { return m_swapchain; } const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; } VkExtent2D GetExtent() const { return m_extent; } + // Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was + // created from - the value to compare a freshly queried currentExtent against. + VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; } VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; } const Vector& GetImages() const { return m_images; } const Vector& GetImageViews() const { return m_imageViews; } @@ -63,6 +66,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; VkSurfaceFormatKHR m_surfaceFormat{}; VkExtent2D m_extent{}; + VkExtent2D m_surfaceExtent{}; VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; Vector m_images; Vector m_imageViews; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index a367b1f7..62ddda8f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2583,12 +2583,10 @@ void main() { m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); } else if (acquireResult == VK_SUBOPTIMAL_KHR) { // The image is usable, and its acquire signal is already armed on - // imageAvailableSemaphore. Re-acquiring here would arm a second - // signal on a binary semaphore whose first one nobody has waited on - // yet; keep the image and let Present rebuild after the frame that - // consumes the signal. - MGLOG_D("Initialize, vkAcquireNextImageKHR got VK_SUBOPTIMAL_KHR; deferring swapchain rebuild"); - m_swapchainResizeRequested = true; + // imageAvailableSemaphore. Re-acquiring here would arm a second signal on a + // binary semaphore whose first one nobody has waited on yet; keep the image. + // Only a real surface change schedules a rebuild. + m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate(); acquireResult = VK_SUCCESS; } VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); @@ -7566,9 +7564,9 @@ void main() { const VkResult acquireResult = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); if (acquireResult == VK_SUBOPTIMAL_KHR) { - // Usable image with its acquire signal already armed; rebuild only - // once a submit has consumed it (see step 4 at the end of Present). - m_swapchainResizeRequested = true; + // Usable image with its acquire signal already armed; a rebuild is scheduled + // only if the surface genuinely no longer matches (see step 4 of Present). + m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate(); } else { VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage"); } @@ -7628,7 +7626,15 @@ void main() { // 2) Present current frame. auto presentPacket = m_frameContext.GetPresentInfo(m_swapchainObject.GetHandle(), m_imageIndexAcquired); auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo); - if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + if (result == VK_SUBOPTIMAL_KHR) { + // Suboptimal is not a reason to rebuild on its own: a driver may report it for a + // surface whose size and orientation still match what we built from (Android does + // this routinely), and rebuilding on it alone destroys every pipeline and + // reallocates the default framebuffer once per frame - flicker, then garbage. + // Defer to the surface-capabilities comparison below. + result = VK_SUCCESS; + } + if (result == VK_ERROR_OUT_OF_DATE_KHR) { MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result); if (!RecreateSwapchain()) { // Window went zero-area (minimize) with the swapchain out of date: @@ -7642,6 +7648,13 @@ void main() { result = VK_SUCCESS; } VK_VERIFY(result, "Present, vkQueuePresentKHR"); + // The authoritative check, done here - after the frame is presented, before the next + // acquire. This is what makes a launcher-side resolution change take effect: shrinking + // the window's buffer (SurfaceHolder.setFixedSize) moves currentExtent, the swapchain + // follows, and the compositor scales the smaller image up to the view for free. + if (!m_swapchainResizeRequested && SwapchainIsOutOfDate()) { + m_swapchainResizeRequested = true; + } if (m_swapchainResizeRequested) { MGLOG_D("Present, processing requested swapchain resize"); if (!RecreateSwapchain()) { @@ -7659,13 +7672,12 @@ void main() { // 4) Wait/reset/acquire for next frame. result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); if (result == VK_SUBOPTIMAL_KHR) { - // An image was acquired and its signal is armed on this slot's - // imageAvailableSemaphore. Rebuilding now would mean re-acquiring on - // that same binary semaphore, arming a second signal while the first is - // still unwaited. Keep the image: the rebuild runs at the top of the - // next Present's step 4 above, once this frame's submit has consumed it. - MGLOG_D("Present, vkAcquireNextImageKHR got VK_SUBOPTIMAL_KHR; deferring swapchain rebuild"); - m_swapchainResizeRequested = true; + // An image WAS acquired and its signal is armed on this slot's + // imageAvailableSemaphore, so the frame proceeds normally. Whether a rebuild is + // actually needed is decided by the surface-capabilities comparison at the next + // Present - suboptimal alone must not schedule one, or a driver that reports it + // every frame would rebuild every frame. + m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate(); result = VK_SUCCESS; } else if (result == VK_ERROR_OUT_OF_DATE_KHR) { // Nothing acquired, nothing signaled: safe to rebuild and re-acquire. @@ -8600,6 +8612,38 @@ void main() { return m_physicalDevice; } + Bool VulkanRenderer::SwapchainIsOutOfDate() { + if (m_surface == VK_NULL_HANDLE || m_swapchainObject.GetHandle() == VK_NULL_HANDLE) { + return false; + } + VkSurfaceCapabilitiesKHR surfaceCaps{}; + if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physicalDevice.handle, m_surface, &surfaceCaps) != + VK_SUCCESS) { + return false; + } + // A driver-defined currentExtent (UINT32_MAX) means the surface takes its size from the + // swapchain, so there is nothing to compare against - the app's requested size wins and + // only an explicit RequestSwapchainResize can change it. + if (surfaceCaps.currentExtent.width == UINT32_MAX || surfaceCaps.currentExtent.height == UINT32_MAX) { + return false; + } + // Compare in SURFACE space against the extent the live swapchain was created from. Using + // the swapchain's own (quarter-turn swapped) extent here would report a difference on + // every rotated frame and rebuild forever. + const VkExtent2D builtFrom = m_swapchainObject.GetSurfaceExtent(); + const Bool extentChanged = surfaceCaps.currentExtent.width != builtFrom.width || + surfaceCaps.currentExtent.height != builtFrom.height; + const Bool transformChanged = surfaceCaps.currentTransform != m_swapchainObject.GetPreTransform(); + if (!extentChanged && !transformChanged) { + return false; + } + MGLOG_I("Swapchain out of date: surface %ux%u transform %u -> %ux%u transform %u", + builtFrom.width, builtFrom.height, static_cast(m_swapchainObject.GetPreTransform()), + surfaceCaps.currentExtent.width, surfaceCaps.currentExtent.height, + static_cast(surfaceCaps.currentTransform)); + return true; + } + void VulkanRenderer::RequestSwapchainResize(Uint32 width, Uint32 height) { width = std::max(width, 1); height = std::max(height, 1); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index bf1172b7..591aa60c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -274,6 +274,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const; void RequestSwapchainResize(Uint32 width, Uint32 height); + // Re-query the surface and report whether the live swapchain no longer matches it + // (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a + // rebuild, so a surface the driver merely considers suboptimal cannot thrash. + Bool SwapchainIsOutOfDate(); // Returns false when the surface is zero-area (minimized/hidden window): // no new swapchain is installed and presentation must stay suspended. Bool RecreateSwapchain();