diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 2fb00688..ff6cf2f8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -568,6 +568,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 swapchainImageIndex = 0; Bool is3D = false; MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle; + // The storage's resource, for a non-default surface (null for the default framebuffer). + VkTextureManager::TextureResource* resource = nullptr; }; // `isWriteTarget` selects which side of the default framebuffer this resolves: a write // re-points it at the image Present() acquired, a read keeps the current one (see @@ -834,6 +836,30 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The current wire pass's attachments were marked GPU-written at this StagedTextureStore // clear generation (SetupWireDraw); false whenever the pass changes. Bool m_wirePassMarksValid = false; + // P14: THE OPEN PASS'S ATTACHMENTS, as the last full attachment walk resolved them. A draw + // into the same framebuffer record (FramebufferSerial moves on every framebuffer write, + // ContextSerial on every applier reset) while the pass is still open on this command buffer, + // with no texture image re-created or erased, and every attachment still in GENERAL with no + // pending upload, would resolve exactly these again and continue the pass: it skips both + // resolves and the pass key. Never for the default framebuffer, whose image index moves + // with each acquire. + struct WirePassMemo { + Bool valid = false; + Uint64 framebufferSerial = 0, contextSerial = 0; + MG_Pipe::MGPipeHandle drawFramebuffer = MG_Pipe::kMGPipeNullHandle; + Uint64 textureImageEpoch = 0, resourceEraseEpoch = 0; + Bool framebufferSrgb = false; + Uint64 passHash = 0; + struct Attachment { + VkTextureManager::TextureResource* resource = nullptr; + MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle; + Bool renderbuffer = false; + }; + Vector attachments; + Vector marks; + }; + WirePassMemo m_wirePassMemo; + Bool WirePassMemoHolds(const MG_Pipe::MGPFramebufferState& fbo, const FrameContext::FrameData& frame) const; Uint64 m_wirePassMarksClearGeneration = 0; WireDrawScratch m_wireDrawScratch; struct WireRetiredObjects { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index 5c1fc7d4..92aab5eb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -106,6 +106,7 @@ m_wireDrawPass.reset(); m_wireDrawPassCompatibilityId = 0; m_wirePassMarksValid = false; + m_wirePassMemo.valid = false; for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr); m_wireDrawViews.clear(); m_wireDrawPassKey = {}; @@ -121,6 +122,7 @@ void VulkanRenderer::RetireWireDrawPass() { m_wireDrawPassCompatibilityId = 0; m_wirePassMarksValid = false; + m_wirePassMemo.valid = false; if (!m_wireDrawPass) return; auto& frame = m_frameContext.GetCurrent(); if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass()) @@ -202,6 +204,37 @@ ~WirePreparationScope() { --depth; } }; + Bool VulkanRenderer::WirePassMemoHolds(const MG_Pipe::MGPFramebufferState& fbo, + const FrameContext::FrameData& frame) const { + const auto& memo = m_wirePassMemo; + if (!memo.valid || fbo.IsDefault || !m_wireDrawPass || !frame.isCommandRecording) return false; + const auto* active = VkRenderPassManager::GetActiveRenderPass(); + if (active == nullptr || active->commandBuffer != frame.commandBuffer || active->hash != m_wireDrawPass->hash || + active->hash != memo.passHash) + return false; + const auto& st = MG_Pipe::MGPipeApplier(); + if (memo.framebufferSerial != st.FramebufferSerial || memo.contextSerial != st.ContextSerial || + !(memo.drawFramebuffer == st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)]) || + memo.textureImageEpoch != m_textureManager->GetTextureImageEpoch() || + memo.resourceEraseEpoch != m_textureManager->GetResourceEraseEpoch() || + memo.framebufferSrgb != MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb)) + return false; + // The resources are good while the erase epoch holds. Each must still be what the walk + // left: in GENERAL and synced to its record's serial with nothing pending. + for (const auto& attachment : memo.attachments) { + const auto* resource = attachment.resource; + if (resource == nullptr || resource->layout != VK_IMAGE_LAYOUT_GENERAL) return false; + const auto& records = attachment.renderbuffer ? st.RenderbufferResources : st.TextureResources; + const auto handle = attachment.storage; + if (handle.Slot >= records.size()) return false; + const auto& record = records[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen || record.Serial != resource->syncedWireSerial || + !record.PendingUploads.empty()) + return false; + } + return true; + } + Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices) { auto& state = MG_Pipe::MGPipeApplier(); @@ -259,15 +292,19 @@ // (WireSharedImage.inc). AcquireNotedSharedImages(); // Lazy image uploads/promotions can submit the old recording. Resolve - // ALL attachments before descriptor binding captures the current cmd. - for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { - const Int32 slot = fbo->DrawBuffers[i]; - if (slot >= 0) (void)ResolveWireImage(*fbo, fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT); + // ALL attachments before descriptor binding captures the current cmd - unless the open + // pass's attachments are provably the ones this draw would resolve (WirePassMemo). + Bool passContinues = WirePassMemoHolds(*fbo, frame); + if (!passContinues) { + for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + const Int32 slot = fbo->DrawBuffers[i]; + if (slot >= 0) (void)ResolveWireImage(*fbo, fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT); + } + if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone) + (void)ResolveWireImage(*fbo, fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT); + else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone) + (void)ResolveWireImage(*fbo, fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT); } - if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone) - (void)ResolveWireImage(*fbo, fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT); - else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone) - (void)ResolveWireImage(*fbo, fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT); m_textureManager->FlushPendingUploads(); const auto vaoHandle = state.BoundVertexElements; if (MG_Pipe::MGPipeHandleIsNull(vaoHandle)) MagmaWireFatal("vertex-elements-record"); @@ -483,7 +520,21 @@ MGL_WIRE_DECLINE_AT(UniformBufferBinding, "the program's uniform/storage bindings could not be bound"); return false; } - + // Asked again: the descriptor resolve may have closed the pass, re-created an image or + // moved a layout. + if (passContinues) passContinues = WirePassMemoHolds(*fbo, frame); + // A memo that holds is the open pass itself (WirePassMemoHolds compared the active pass). + Bool continuing = true; + if (passContinues) { + auto& store = MG_Record::ServerStagedTexture(); + if (!m_wirePassMarksValid || m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) { + for (const auto& mark : m_wirePassMemo.marks) + m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers); + m_wirePassMarksValid = true; + m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration(); + } + } else { + m_wirePassMemo.valid = false; auto& attachments = scratch.attachments; auto& viewInfos = scratch.viewInfos; auto& colors = scratch.colors; @@ -509,10 +560,13 @@ auto& sameLayoutAttachments = scratch.sameLayoutAttachments; sameLayoutAttachments.clear(); scratch.gpuWriteMarks.clear(); + m_wirePassMemo.attachments.clear(); const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 { WireImage image = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/true); if (!image.image) return VK_ATTACHMENT_UNUSED; m_textureManager->FlushPendingUploads(); + m_wirePassMemo.attachments.push_back( + {image.resource, image.storage, surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer}); if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) image.format = ResolveSrgbAttachmentWriteFormat(image.format, MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb)); @@ -588,7 +642,7 @@ passInfo.pAttachments = attachments.data(); passInfo.subpassCount = 1; passInfo.pSubpasses = &subpass; - const Bool continuing = m_wireDrawPass && m_wireDrawPassKey == passKey; + continuing = m_wireDrawPass && m_wireDrawPassKey == passKey; { const auto* active = VkRenderPassManager::GetActiveRenderPass(); const Bool stillOpen = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer; @@ -642,8 +696,21 @@ m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration(); } } - auto& pass = *m_wireDrawPass; - // Interned once per pass: a continued pass has the same key, so the same compatibility. + { + // What this walk proved, for WirePassMemoHolds on the next draw. + const auto& st = MG_Pipe::MGPipeApplier(); + auto& memo = m_wirePassMemo; + memo.valid = !fbo->IsDefault; + memo.framebufferSerial = st.FramebufferSerial; + memo.contextSerial = st.ContextSerial; + memo.drawFramebuffer = st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)]; + memo.textureImageEpoch = m_textureManager->GetTextureImageEpoch(); + memo.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch(); + memo.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb); + memo.passHash = m_wireDrawPass->hash; + memo.marks.assign(scratch.gpuWriteMarks.begin(), scratch.gpuWriteMarks.end()); + } + // The compatibility key is built from this walk's attachments only when the pass is new. if (m_wireDrawPassCompatibilityId == 0) { auto& pipelinePassKey = scratch.compatibilityKey; pipelinePassKey.attachmentFormatsAndSamples.clear(); @@ -655,6 +722,9 @@ pipelinePassKey.depthReference = depth.attachment; m_wireDrawPassCompatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey); } + } // !passContinues + auto& pass = *m_wireDrawPass; + // Interned once per pass (above): a continued pass has the same compatibility. const Uint64 compatibilityId = m_wireDrawPassCompatibilityId; const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, ResolvePrimitiveRestartEnable(aspects, indices), compatibilityId); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 4123f3d0..898a65e7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -172,6 +172,7 @@ std::max(resource->extent.height >> level, 1u)}; out.layout = resource->layout; out.trackedLayout = &resource->layout; + out.resource = resource; out.samples = resource->sampleCount; // P7 wave 2-B2: the shader multisample resolve SAMPLES this image, which is a question // about the image and not about the format - VkTextureManager asks for SAMPLED on every