From add15f0a17c499eeb0da06453574c4547e7ef0f3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 9 Oct 2026 23:36:25 -0400 Subject: [PATCH] [Perf] (Magma): submit off-screen work ahead of the swapchain acquire wait --- .../DirectVulkan/Renderer/FrameContext.cpp | 8 +++- .../DirectVulkan/Renderer/FrameContext.h | 15 ++++++- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 44 ++++++++++++++++--- .../DirectVulkan/Renderer/VulkanRenderer.h | 10 ++++- .../DirectVulkan/Renderer/WireFramebuffer.inc | 12 +++++ 5 files changed, 80 insertions(+), 9 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp index 72cd9a0e..62bc07a3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp @@ -302,7 +302,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { auto& frame = GetCurrent(); const VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore, VK_NULL_HANDLE, &outImageIndex); - if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) frame.imageAvailableSemaphoreConsumed = false; + if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) { + frame.imageAvailableSemaphoreConsumed = false; + frame.acquiredImageReferenced = false; + } return result; } @@ -333,6 +336,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } frame.imageAvailableSemaphoreConsumed = false; + frame.acquiredImageReferenced = false; const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence); // Hand the acquire's own code back so the caller can schedule a rebuild. return resetResult == VK_SUCCESS ? result : resetResult; @@ -476,6 +480,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { frame.hasCommandBufferRecorded = false; frame.isCommandRecording = false; frame.imageAvailableSemaphoreConsumed = false; + frame.acquiredImageReferenced = false; return VK_SUCCESS; } @@ -494,5 +499,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { frame.isCommandRecording = false; frame.hasCommandBufferRecorded = false; frame.imageAvailableSemaphoreConsumed = false; + frame.acquiredImageReferenced = false; } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h index c0a6fa84..b4c475c0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h @@ -25,8 +25,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { virtual void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) = 0; }; + // The stages the acquire wait blocks. Every first access of an acquired swapchain image is + // a layout transition out of PRESENT_SRC_KHR or UNDEFINED whose source scope includes these + // stages (TransitionWireImage, TransitionToPresent), and that chain is what orders the + // access after the presentation engine's release. Work at other stages, in this and later + // submissions, does not wait for the release. + static constexpr VkPipelineStageFlags kAcquireWaitStages = + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT; + struct SubmitInfoPacket { - VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkPipelineStageFlags waitDstStageMask = kAcquireWaitStages; VkSemaphore waitSemaphore = VK_NULL_HANDLE; VkSemaphore signalSemaphore = VK_NULL_HANDLE; // [0] = pre-pass command buffer (when recorded), then the frame @@ -67,6 +75,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool isPreCommandRecording = false; Bool hasPreCommandBufferRecorded = false; Bool imageAvailableSemaphoreConsumed = false; + // The open recording references the acquired swapchain image. Only a submission that + // does (or the present's) waits on imageAvailableSemaphore: the frame's work recorded + // before its first use of the image goes to the queue without the wait, so it never + // queues behind the release of an image the display still scans out. + Bool acquiredImageReferenced = false; // Command buffers submitted mid-frame (FlushPendingCommands), // appended in submit order; freed once their submission is known // complete (fence wait or completion poll). diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 297d88c5..0938e8eb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3601,6 +3601,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // split server records every draw its client sends into the same frame command buffer, so // the same loading frame grows it the same way there. SplitOversizedRecording(); + // A draw into the window: the frame's work so far goes out now, ahead of the acquire wait, + // while nothing of this draw is recorded yet (its descriptor binds must land in the + // recording the draw does). ResolveWireImage would split later, mid-setup. + if (const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); fbo != nullptr && fbo->IsDefault) + SubmitAheadOfAcquiredImage(); if (!RewindWireDescriptorSetsIfDue()) return false; // The wire route returns before the monolith branch's draw-gated // sweep. Dead wire texture/renderbuffer records otherwise live @@ -6116,7 +6121,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_completedSubmitCounter = m_submitCounter; } - Bool VulkanRenderer::SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence) { + Bool VulkanRenderer::SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence, + Bool waitAcquire) { RetireWireDrawPass(); // Batched texture uploads must reach the queue before the frame's // commands: the recording being submitted may sample images whose @@ -6124,10 +6130,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (m_textureManager) { m_textureManager->FlushPendingUploads(); } - VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; + VkPipelineStageFlags waitDstStageMask = FrameContext::kAcquireWaitStages; VkSemaphore waitSemaphore = frame.imageAvailableSemaphore; VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO}; - if (!frame.imageAvailableSemaphoreConsumed) { + // A recording that never touched the acquired image does not wait for its release: on a + // queue that runs submissions in order, that wait would hold this work, and everything + // after it, until the display lets go of the image (SubmitAheadOfAcquiredImage). + const Bool waitsAcquire = + !frame.imageAvailableSemaphoreConsumed && (frame.acquiredImageReferenced || waitAcquire); + if (waitsAcquire) { submitInfo.waitSemaphoreCount = 1; submitInfo.pWaitSemaphores = &waitSemaphore; submitInfo.pWaitDstStageMask = &waitDstStageMask; @@ -6161,7 +6172,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { MGLOG_E_ONCE("SubmitPendingCommandBuffer: vkQueueSubmit returned %d", result); return false; } - frame.imageAvailableSemaphoreConsumed = true; + if (waitsAcquire) frame.imageAvailableSemaphoreConsumed = true; frame.hasCommandBufferRecorded = false; frame.hasPreCommandBufferRecorded = false; RegisterSubmit(fence, pooledFence); @@ -6239,6 +6250,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + void VulkanRenderer::SubmitAheadOfAcquiredImage() { + if (m_frameContext.GetFrameCount() == 0) return; + auto& frame = m_frameContext.GetCurrent(); + if (frame.imageAvailableSemaphoreConsumed || frame.acquiredImageReferenced) return; + if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended) return; + // The frame's work so far renders off screen. Submitted together with the window pass it + // would wait behind the acquire semaphore, which signals only when the display releases + // the image - about every other frame that is the image it has just stopped scanning out, + // so the whole frame's GPU work started after the next display commit. Submitted now it + // runs while the display still holds the image. A failed flush leaves the work recorded, + // and it goes out later with the wait, as before. Inside a draw's preparation nothing is + // submitted (the draw may already hold state of the open recording); the image is only + // marked, so the submission that carries the use still waits. + if (m_wirePreparationDepth == 0 && HasPendingRecordedWork() && FlushPendingCommands()) + MGLOG_I_ONCE("DirectVulkan: a frame's off-screen work goes to the queue ahead of its window pass; only " + "the submission that uses the swapchain image waits for its acquire"); + frame.acquiredImageReferenced = true; + } + void VulkanRenderer::NoteDeviceLoss(VkResult result, const char* where) { if (result != VK_ERROR_DEVICE_LOST || m_deviceLost) return; m_deviceLost = true; @@ -6555,6 +6585,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { } MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(), "Present, acquired image index out of range"); + // A frame that never drew into the window still presents its image: what it recorded + // goes out first, unwaited, and the present's own submission waits for the release. + SubmitAheadOfAcquiredImage(); // 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; @@ -9020,7 +9053,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE && !m_presentSuspended && !frame.imageAvailableSemaphoreConsumed) { const VkFence fence = AcquirePooledSubmitFence(); - if (fence == VK_NULL_HANDLE || !SubmitPendingCommandBuffer(frame, fence, /*pooledFence=*/true)) { + if (fence == VK_NULL_HANDLE || + !SubmitPendingCommandBuffer(frame, fence, /*pooledFence=*/true, /*waitAcquire=*/true)) { MGLOG_E("DirectVulkan: a parked surface's pending acquire could not be waited out"); } } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index d9c938fb..7f511234 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -976,12 +976,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { // vkWaitForFences inline before returning. void RegisterSubmit(VkFence fence, Bool pooledFence); // Builds the submit packet for the frame's pending command buffer - // (consuming the acquire semaphore on the slot's first submission), + // (consuming the acquire semaphore on the first submission that + // references the acquired image, or on any when `waitAcquire`), // submits it with `fence`, and registers the submission. On failure // the frame state is left untouched. Shared by the mid-frame flush // and the readback path so the semaphore-consumption invariant lives // in one place. - Bool SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence); + Bool SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence, + Bool waitAcquire = false); + // Called before the frame first references the acquired swapchain image: submits what is + // recorded so far WITHOUT the acquire wait, then marks the recording as referencing the + // image, so only the window pass waits for the presentation engine's release. + void SubmitAheadOfAcquiredImage(); // Polls in-flight submission fences (prefix order) and advances the // completed counter past every fence observed signaled. void RefreshCompletedSubmits(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 34df1a32..9efe6db1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -133,6 +133,9 @@ out.swapchainImageIndex = isWriteTarget ? DefaultFramebufferWriteIndex(true) : DefaultFramebufferReadIndex(true); if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) { + // Every use of the acquired image is reached through here, ahead of its commands: + // the work recorded before it goes out without the acquire wait. + if (out.swapchainImageIndex == m_imageIndexAcquired) SubmitAheadOfAcquiredImage(); out.image = m_swapchainObject.GetImage(out.swapchainImageIndex); out.format = m_swapchainObject.GetSurfaceFormat().format; out.layout = m_swapchainObject.GetImageLayout(out.swapchainImageIndex); @@ -201,6 +204,12 @@ VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags access = 0; GetImageTransitionSourceState(image.layout, stage, access); + // A swapchain image leaves PRESENT_SRC_KHR (or UNDEFINED, new) only after it was acquired: + // the transition is ordered after the presentation engine's release through the stages + // the acquire wait blocks, which an empty (TOP_OF_PIPE) source scope would not chain to. + if (image.isDefault && image.aspect == VK_IMAGE_ASPECT_COLOR_BIT && + (image.layout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR || image.layout == VK_IMAGE_LAYOUT_UNDEFINED)) + stage |= FrameContext::kAcquireWaitStages; // One tracked layout per image: move the complete mip chain, never just the surface. VkImageAspectFlags barrierAspect = image.aspect; // Every packed format, D16_UNORM_S8_UINT included (g5-msrbo review round): the layout is @@ -565,6 +574,9 @@ void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, const ClearAttachmentPayload& clearRequest, GLint drawbuffer) { if (MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return; + // A clear of the window: the frame's work so far goes out ahead of the acquire wait, before + // any of the clear's paths (in-pass, deferred, standalone) records or resolves anything. + if (fbo.IsDefault) SubmitAheadOfAcquiredImage(); // P15 S0: what the draw pass can take is cleared inside it (ClearWireFramebufferInPass, // WireDraw.inc); only the buffers it hands back take the standalone pass below. // P15: in the open pass when it is this framebuffer's (the in-pass clear) or merely holds