From c353a2055fb0dbd4dd3f23f927c2ac3afd600ad9 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 30 Jul 2026 02:43:13 -0400 Subject: [PATCH] [Feat] (DirectVulkan): pre-pass command stream for reorderable out-of-pass work - a draw whose sampled texture needs out-of-pass work (deferred clear materialization or a sampled-layout transition) used to end the active render pass - a full-target store+reload on a tiler - even when the only ordering the work needs is 'before this draw'; MC 26.2 clears an overlay texture every frame and samples it mid-pass, splitting the main scene pass once per frame for nothing - every frame slot now carries a second primary command buffer, submitted strictly AHEAD of the frame command buffer in the same vkQueueSubmit; when the open recording has not referenced the image yet (tracked via a recording-generation stamp on the texture resource, advanced on every frame-command-buffer begin and stamped at every recorded reference: attachments at BeginRenderPass/attachment-write, sampled reads per draw, layout transitions), the clear/transition is recorded there and the active pass stays open - ANGLE's outside-render-pass command stream, restricted to the provably reorderable case - mid-frame flushes and readback submits close and carry the pre stream with the frame buffer (it must never be submitted later than the recording it was paired with), retiring both under the same submit index; dropped recordings (present suspension, swapchain recreation) abandon it - MaterializePendingClearForTexture's no-active-render-pass assert now applies only to the frame command buffer, since the pre stream records while a pass is open on the frame buffer by design --- .../DirectVulkan/Renderer/FrameContext.cpp | 79 ++++++++++++++++--- .../DirectVulkan/Renderer/FrameContext.h | 22 +++++- .../Renderer/VkRenderPassManager.cpp | 11 +++ .../Renderer/VkTextureManager.cpp | 16 ++++ .../DirectVulkan/Renderer/VkTextureManager.h | 26 ++++++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 76 ++++++++++++++++-- 6 files changed, 211 insertions(+), 19 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp index ab87c405..145aa72b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp @@ -16,18 +16,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_device = device; m_commandPool = commandPool; - Vector commandBuffers(frameCount, VK_NULL_HANDLE); + Vector commandBuffers(frameCount * 2, VK_NULL_HANDLE); VkCommandBufferAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.commandPool = commandPool; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = frameCount; + allocInfo.commandBufferCount = frameCount * 2; VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()); if (result != VK_SUCCESS) { return result; } for (Uint32 i = 0; i < frameCount; ++i) { m_frames[i].commandBuffer = commandBuffers[i]; + m_frames[i].preCommandBuffer = commandBuffers[frameCount + i]; } VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; @@ -47,9 +48,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) { const Uint32 frameCount = static_cast(m_frames.size()); - Vector commandBuffers(frameCount, VK_NULL_HANDLE); + Vector commandBuffers(frameCount * 2, VK_NULL_HANDLE); for (Uint32 i = 0; i < frameCount; ++i) { commandBuffers[i] = m_frames[i].commandBuffer; + commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer; } for (Uint32 i = 0; i < frameCount; ++i) { @@ -60,7 +62,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { for (auto& frame : m_frames) { FreeRetiredCommandBuffers(frame); } - vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data()); + vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data()); } m_frames.clear(); currentFrameIndex = 0; @@ -87,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { currentFrameIndex = (currentFrameIndex + 1) % static_cast(m_frames.size()); GetCurrent().isCommandRecording = false; GetCurrent().hasCommandBufferRecorded = false; + GetCurrent().isPreCommandRecording = false; + GetCurrent().hasPreCommandBufferRecorded = false; } VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags, @@ -118,6 +122,41 @@ namespace MobileGL::MG_Backend::DirectVulkan { frame.hasCommandBufferRecorded = true; } + VkCommandBuffer FrameContext::BeginPreCommandRecording() { + auto& frame = GetCurrent(); + if (frame.isPreCommandRecording) { + return frame.preCommandBuffer; + } + MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded, + "BeginPreCommandRecording: a recorded pre stream is still awaiting submission"); + VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer"); + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo), + "BeginPreCommandRecording, vkBeginCommandBuffer"); + frame.isPreCommandRecording = true; + return frame.preCommandBuffer; + } + + void FrameContext::EndPreCommandRecordingIfOpen() { + auto& frame = GetCurrent(); + if (!frame.isPreCommandRecording) { + return; + } + VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer"); + frame.isPreCommandRecording = false; + frame.hasPreCommandBufferRecorded = true; + } + + void FrameContext::AbandonPreCommandRecording() { + auto& frame = GetCurrent(); + if (frame.isPreCommandRecording) { + VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer"); + } + frame.isPreCommandRecording = false; + frame.hasPreCommandBufferRecorded = false; + } + VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) { DestroySwapchainSemaphores(device); if (swapchainImageCount == 0) { @@ -202,17 +241,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 swapchainImageIndex) const { const auto& frame = GetCurrent(); MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active"); + MOBILEGL_ASSERT(!frame.isPreCommandRecording, + "GetSubmitInfo called while the pre-pass stream is still recording"); AssertValidSwapchainImageIndex(swapchainImageIndex); SubmitInfoPacket packet{}; packet.waitSemaphore = frame.imageAvailableSemaphore; packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex]; - packet.commandBuffer = frame.commandBuffer; + + Uint32 commandBufferCount = 0; + // The pre-pass stream executes strictly before the frame's commands. + if (frame.hasPreCommandBufferRecorded) { + packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer; + } + if (shouldSubmitCommandBuffer) { + packet.commandBuffers[commandBufferCount++] = frame.commandBuffer; + } packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U; packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore; packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask; - packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U; - packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr; + packet.submitInfo.commandBufferCount = commandBufferCount; + packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr; packet.submitInfo.signalSemaphoreCount = 1; packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore; return packet; @@ -276,12 +325,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_recordingObserver = observer; } - VkResult FrameContext::RetireCurrentCommandBuffer() { + VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) { MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE, "RetireCurrentCommandBuffer requires an initialized FrameContext"); auto& frame = GetCurrent(); MOBILEGL_ASSERT(!frame.isCommandRecording, "RetireCurrentCommandBuffer called while the command buffer is still recording"); + MOBILEGL_ASSERT(!frame.isPreCommandRecording, + "RetireCurrentCommandBuffer called while the pre-pass stream is still recording"); VkCommandBufferAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; @@ -289,10 +340,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.commandBufferCount = 1; VkCommandBuffer replacement = VK_NULL_HANDLE; - const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement); + VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement); if (result != VK_SUCCESS) { return result; } + if (retirePreCommandBuffer) { + VkCommandBuffer preReplacement = VK_NULL_HANDLE; + result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement); + if (result != VK_SUCCESS) { + vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement); + return result; + } + frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex}); + frame.preCommandBuffer = preReplacement; + } // lastSubmitIndex was just written by the renderer for the submission // that carried this command buffer. frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex}); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h index af83e379..c12f30bc 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h @@ -29,7 +29,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkSemaphore waitSemaphore = VK_NULL_HANDLE; VkSemaphore signalSemaphore = VK_NULL_HANDLE; - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; + // [0] = pre-pass command buffer (when recorded), then the frame + // command buffer; submitInfo.pCommandBuffers points here. + VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO}; }; @@ -52,10 +54,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { struct FrameData { VkCommandBuffer commandBuffer = VK_NULL_HANDLE; + // Pre-pass work stream: out-of-pass commands (deferred clear + // materialization, sampled-layout transitions) for resources the + // frame's recording has not touched yet. Submitted immediately + // BEFORE commandBuffer in the same vkQueueSubmit, so recording + // into it never has to split the frame's active render pass. + VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE; VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE; VkFence imageInFlightFence = VK_NULL_HANDLE; Bool isCommandRecording = false; Bool hasCommandBufferRecorded = false; + Bool isPreCommandRecording = false; + Bool hasPreCommandBufferRecorded = false; Bool imageAvailableSemaphoreConsumed = false; // Command buffers submitted mid-frame (FlushPendingCommands), // appended in submit order; freed once their submission is known @@ -77,6 +87,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0, const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr); void EndCommandRecording(); + // Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer). + VkCommandBuffer BeginPreCommandRecording(); + // Closes the pre stream if open, marking it for submission ahead of the + // frame command buffer. Safe to call when it never opened. + void EndPreCommandRecordingIfOpen(); + // Drops an in-progress or recorded-but-unsubmitted pre stream (dropped + // frame recordings, swapchain recreation). + void AbandonPreCommandRecording(); VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount); void DestroySwapchainSemaphores(VkDevice device); Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout, @@ -91,7 +109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // can restart while the submitted buffer is still executing. Retired // buffers are freed after the slot's fence is next waited, or as soon // as their submission is observed complete. - VkResult RetireCurrentCommandBuffer(); + VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false); // Frees every retired command buffer whose tagged submission index is // known complete. Driven by the renderer's submit tracker on completion diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 86a2726e..99c0671a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -1395,6 +1395,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { renderPassBeginInfo.pClearValues = clearValues.data(); vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + // Pre-pass stream bookkeeping: this pass's attachment images are now + // referenced by the open frame recording. + if (s_textureManager != nullptr) { + for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) { + if (tracked.target == TrackedAttachmentTarget::Texture) { + if (const auto texture = tracked.texture.lock()) { + s_textureManager->StampTextureRecordingUse(texture.get()); + } + } + } + } for (const auto& pending: renderPassEntry.pendingClearAttachments) { if (pending.hasInlinePayload) { if (s_renderPassManager != nullptr) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index a8252d5b..2972f9b8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1049,6 +1049,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { return view; } + void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) { + if (texture == nullptr) { + return; + } + auto it = m_textureResources.find(MakeTextureIdentity(texture)); + if (it != m_textureResources.end()) { + it->second.lastRecordingGeneration = m_recordingGeneration; + } + } + void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) { MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null"); auto it = m_textureResources.find(MakeTextureIdentity(texture)); @@ -1076,6 +1086,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels, "UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u", texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels); + // Pre-pass stream bookkeeping: the render pass that just ended wrote this image. + StampResourceRecordingUse(resource); if (resource.layout != newLayout && resource.mipLevels > 1) { VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; @@ -1160,6 +1172,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels, resource->arrayLayers); MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex()); + // Pre-pass stream bookkeeping: a command referencing the image was recorded. + StampResourceRecordingUse(*resource); return ok; } @@ -1189,6 +1203,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource->aspect, 0, resource->mipLevels, resource->arrayLayers); MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d", texture.GetExternalIndex()); + // Pre-pass stream bookkeeping: a command referencing the image was recorded. + StampResourceRecordingUse(*resource); return ok; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 4c47f15e..713c643b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -172,6 +172,13 @@ public: // NeedsStorageImagePreparation cannot ask for a recreate that will never happen. Bool storageUsageResolved = false; Uint16 syncedTextureParamsVersion = 0; + // Recording generation (VkTextureManager::GetRecordingGeneration) of the last + // command referencing this image that was recorded into the CURRENT frame + // command buffer. An image untouched by the open recording may have its + // out-of-pass work (deferred clears, sampled-layout transitions) recorded + // into the frame's PRE command buffer - which executes strictly before the + // frame's commands - instead of splitting the active render pass. + Uint64 lastRecordingGeneration = 0; // Snapshot of ITextureObject::GetContentVersion() at the last successful sync; // lets SyncTexture skip the whole re-check/re-upload when content is unchanged. Uint64 syncedContentVersion = 0; @@ -207,6 +214,7 @@ public: std::swap(this->usageFlags, that.usageFlags); std::swap(this->storageUsageResolved, that.storageUsageResolved); std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion); + std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration); std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); } @@ -307,6 +315,21 @@ public: VkImageLayout newLayout); Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); + + // Recording-generation bookkeeping for the pre-pass command stream. The + // generation advances every time the frame command buffer (re)begins + // recording; a resource whose stamp does not match was not referenced by + // any command in the open recording, so its out-of-pass work may safely + // execute ahead of the whole recording (in the pre command buffer). + void AdvanceRecordingGeneration() { ++m_recordingGeneration; } + void StampResourceRecordingUse(TextureResource& resource) const { + resource.lastRecordingGeneration = m_recordingGeneration; + } + // Map-lookup variant for callers that only hold the GL texture object. + void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture); + Bool WasTouchedThisRecording(const TextureResource& resource) const { + return resource.lastRecordingGeneration == m_recordingGeneration; + } // Records that this texture is bound to a GL image unit, so its image must carry // VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and // therefore before the render pass is committed: an image that has to be upgraded is @@ -364,6 +387,9 @@ public: private: // Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch(). Uint64 m_textureImageEpoch = 1; + // See AdvanceRecordingGeneration. Starts above every resource's default + // stamp of 0 so a fresh resource counts as untouched. + Uint64 m_recordingGeneration = 1; Bool SyncTexture(MG_State::GLState::ITextureObject &texture, TextureResource &outResource); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index b0ba9659..410723b0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -4397,8 +4397,30 @@ void main() { static_cast(textureResource->layout)); if (m_clearManager->HasPendingClear(sampledTexture) || !IsValidSampledImageLayout(textureResource->layout)) { + // Out-of-pass work is needed (deferred clear materialization or + // a sampled-layout transition). When the open frame recording + // has not referenced this image yet, that work can execute + // ahead of the WHOLE recording - record it into the pre-pass + // stream instead of splitting the active render pass (ANGLE's + // outside-render-pass command stream, restricted to the + // provably reorderable case). + if (activeRenderPass != nullptr && + !m_frameContext.GetCurrent().hasPreCommandBufferRecorded && + !m_textureManager->WasTouchedThisRecording(*textureResource)) { + VkCommandBuffer preCommandBuffer = m_frameContext.BeginPreCommandRecording(); + const Bool preClearReady = + MaterializePendingClearForTexture(preCommandBuffer, *sampledTexture); + MOBILEGL_ASSERT(preClearReady, + "%s: pre-pass MaterializePendingClearForTexture failed for textureId=%d", + __func__, sampledTexture->GetExternalIndex()); + const Bool preTransitionReady = + m_textureManager->TransitionTextureForSampling(preCommandBuffer, *sampledTexture); + MOBILEGL_ASSERT(preTransitionReady, + "%s: pre-pass TransitionTextureForSampling failed for textureId=%d", + __func__, sampledTexture->GetExternalIndex()); + continue; + } needSampledTextureTransitions = true; - break; } } @@ -4422,6 +4444,10 @@ void main() { MOBILEGL_ASSERT(transitionedResource != nullptr, "%s: post-transition SyncTextureAndGetDescriptor failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); + // Pre-pass stream bookkeeping: the draw about to be recorded reads + // this image, so later out-of-pass work on it can no longer jump + // ahead of the recording. + m_textureManager->StampResourceRecordingUse(*transitionedResource); MGLOG_D("SetupDraw: sampled textureId=%d layout(after)=%s(%d)", sampledTexture->GetExternalIndex(), VkImageLayoutToString(transitionedResource->layout), static_cast(transitionedResource->layout)); @@ -5230,8 +5256,12 @@ void main() { if (!m_clearManager->GetPendingClears(&texture, pendingClears)) { return true; } - MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr, - "MaterializePendingClearForTexture requires no active render pass"); + // A pass may stay open on the FRAME command buffer while this clear is + // recorded into the pre-pass stream (a different command buffer that + // executes strictly before the frame's commands). + MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr || + commandBuffer != m_frameContext.GetCurrent().commandBuffer, + "MaterializePendingClearForTexture requires no active render pass on the target buffer"); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); MOBILEGL_ASSERT(resource != nullptr, @@ -6276,7 +6306,11 @@ void main() { frame.hasCommandBufferRecorded = true; m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo } - if (!frame.hasCommandBufferRecorded) { + // The pre-pass stream must never be submitted later than the recording + // it was paired with (frame commands recorded after a pre-pass move + // rely on the moved work having executed first). + m_frameContext.EndPreCommandRecordingIfOpen(); + if (!frame.hasCommandBufferRecorded && !frame.hasPreCommandBufferRecorded) { return true; } @@ -7411,8 +7445,18 @@ void main() { submitInfo.pWaitSemaphores = &waitSemaphore; submitInfo.pWaitDstStageMask = &waitDstStageMask; } - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &frame.commandBuffer; + // The pre-pass stream, when recorded, executes strictly before the + // frame's commands within the same submission. + VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; + Uint32 commandBufferCount = 0; + if (frame.hasPreCommandBufferRecorded) { + commandBuffers[commandBufferCount++] = frame.preCommandBuffer; + } + if (frame.hasCommandBufferRecorded) { + commandBuffers[commandBufferCount++] = frame.commandBuffer; + } + submitInfo.commandBufferCount = commandBufferCount; + submitInfo.pCommandBuffers = commandBuffers; const VkResult result = vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, fence); if (result != VK_SUCCESS) { MGLOG_E("SubmitPendingCommandBuffer: vkQueueSubmit returned %d", result); @@ -7420,6 +7464,7 @@ void main() { } frame.imageAvailableSemaphoreConsumed = true; frame.hasCommandBufferRecorded = false; + frame.hasPreCommandBufferRecorded = false; RegisterSubmit(fence, pooledFence); frame.lastSubmitIndex = m_submitCounter; return true; @@ -7450,6 +7495,8 @@ void main() { } m_frameContext.EndCommandRecording(); } + m_frameContext.EndPreCommandRecordingIfOpen(); + const Bool submittingPreCommandBuffer = frame.hasPreCommandBufferRecorded; if (!SubmitPendingCommandBuffer(frame, fence, /*pooledFence=*/true)) { // Submit failure (device loss regime): the ended command buffer // stays marked recorded so Present can still try to submit it. @@ -7468,7 +7515,7 @@ void main() { // The submitted command buffer may still be executing; recording must // restart on a fresh one. If none can be allocated, fall back to // draining this submission so reusing the buffer stays legal. - const VkResult retireResult = m_frameContext.RetireCurrentCommandBuffer(); + const VkResult retireResult = m_frameContext.RetireCurrentCommandBuffer(submittingPreCommandBuffer); if (retireResult != VK_SUCCESS) { MGLOG_E("FlushPendingCommands: RetireCurrentCommandBuffer returned %d; draining submission", retireResult); if (vkWaitForFences(m_device, 1, &fence, VK_TRUE, UINT64_MAX) == VK_SUCCESS) { @@ -7534,6 +7581,11 @@ void main() { } void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) { + // Pre-pass stream bookkeeping: a fresh frame recording references no + // textures yet. + if (m_textureManager) { + m_textureManager->AdvanceRecordingGeneration(); + } if (m_timerQueryManager) { m_timerQueryManager->OnFrameCommandRecordingBegan(commandBuffer, m_frameContext.GetCurrentFrameIndex(), m_bufferManager.GetFrameSerial()); @@ -7606,6 +7658,7 @@ void main() { if (suspendedFrame.isCommandRecording) { m_frameContext.EndCommandRecording(); } + m_frameContext.AbandonPreCommandRecording(); suspendedFrame.isCommandRecording = false; suspendedFrame.hasCommandBufferRecorded = false; m_lastPipelineValid = false; @@ -7670,16 +7723,19 @@ void main() { frame.hasCommandBufferRecorded = true; m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo } + m_frameContext.EndPreCommandRecordingIfOpen(); const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded; - // 1) Submit current frame work. + // 1) Submit current frame work (the pre-pass stream, when recorded, + // rides the same submission strictly ahead of the frame commands). auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired); VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitPacket.submitInfo, frame.imageInFlightFence)); RegisterSubmit(frame.imageInFlightFence, /*pooledFence=*/false); frame.lastSubmitIndex = m_submitCounter; frame.isCommandRecording = false; frame.hasCommandBufferRecorded = false; + frame.hasPreCommandBufferRecorded = false; m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); // 2) Present current frame. @@ -8673,6 +8729,10 @@ void main() { if (m_frameContext.GetFrameCount() > 0) { m_frameContext.GetCurrent().isCommandRecording = false; m_frameContext.GetCurrent().hasCommandBufferRecorded = false; + // The pre-pass stream paired with the abandoned recording is + // dropped with it (its next Begin resets the buffer). + m_frameContext.GetCurrent().isPreCommandRecording = false; + m_frameContext.GetCurrent().hasPreCommandBufferRecorded = false; } const Bool okArena = m_bufferManager.RecreateTransientArenas(m_frameContext.GetFrameCount()); MOBILEGL_ASSERT(okArena, "RecreateSwapchain: buffer manager transient arena initialization failed");