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");