[Perf] (Magma): submit off-screen work ahead of the swapchain acquire wait

This commit is contained in:
2026-10-10 04:14:40 -04:00
parent f4f2bec711
commit add15f0a17
5 changed files with 80 additions and 9 deletions
@@ -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
@@ -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).
@@ -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");
}
}
@@ -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();
@@ -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