diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index d7a59c3c..9b56e9e8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -758,6 +758,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { WireColorBlitResources* m_wireColorBlitResources = nullptr; void DestroyWireColorBlitResources(); void ResetWireColorBlitFramePool(Uint32 frameIndex); + void RetireWireColorBlitTargets(Bool all); // P7 wave 2-B, CONTRACT-P7 ยง5.1 (A): the baked depth-mip program (WireDepthMipmap.inc). // One level of a depth chain, source and destination being two depth subresources of // the same image; the caller loops. It owns no frontend object, which is what the diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc index b3d08771..e58bcf00 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc @@ -19,8 +19,51 @@ // destroyed per blit was ~0.16 ms a blit on Adreno 750. A full pool falls back to that. static constexpr Uint32 kFramePoolSets = 64; Vector framePools; + // P15: the blit's two views and its framebuffer, kept across frames. MC blits its main + // target to the window every frame, and creating and retiring two VkImageViews and a + // VkFramebuffer per blit was ~0.03 ms/frame on the apply thread. The key carries the + // image epochs (any texture image recreated or resource erased invalidates every entry, + // the same rule as the draw-pass cache) and the swapchain image for the default + // framebuffer; ClearAllWireDrawPassCaches drops the lot at the idle points. + struct CachedTarget { + VkImage srcImage = VK_NULL_HANDLE, dstImage = VK_NULL_HANDLE; + Uint32 srcLevel = 0, srcLayer = 0, dstLevel = 0, dstLayer = 0; + VkFormat srcFormat = VK_FORMAT_UNDEFINED, dstFormat = VK_FORMAT_UNDEFINED; + VkExtent2D dstExtent{}; + VkRenderPass renderPass = VK_NULL_HANDLE; + Uint64 textureImageEpoch = 0, resourceEraseEpoch = 0; + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + Bool dstIsDefault = false; + VkImageView srcView = VK_NULL_HANDLE, dstView = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + }; + static constexpr SizeT kCachedTargets = 8; + Vector cachedTargets; }; + // Retires cached blit targets through the wire's deferred destruction (they may still be + // named by an in-flight submission). `all` false keeps the entries whose epochs still hold. + void VulkanRenderer::RetireWireColorBlitTargets(Bool all) { + if (!m_wireColorBlitResources) return; + auto& cached = m_wireColorBlitResources->cachedTargets; + const Uint64 imageEpoch = m_textureManager ? m_textureManager->GetTextureImageEpoch() : 0; + const Uint64 eraseEpoch = m_textureManager ? m_textureManager->GetResourceEraseEpoch() : 0; + WireRetiredObjects retired{}; + SizeT kept = 0; + for (SizeT i = 0; i < cached.size(); ++i) { + auto& entry = cached[i]; + if (!all && entry.textureImageEpoch == imageEpoch && entry.resourceEraseEpoch == eraseEpoch) { + cached[kept++] = entry; + continue; + } + retired.framebuffers.push_back(entry.framebuffer); + retired.imageViews.push_back(entry.srcView); + retired.imageViews.push_back(entry.dstView); + } + cached.resize(kept); + if (!retired.framebuffers.empty()) RetireWireObjects(Move(retired)); + } + void VulkanRenderer::ResetWireColorBlitFramePool(Uint32 frameIndex) { auto& pools = m_wireColorBlitResources->framePools; if (frameIndex < pools.size() && pools[frameIndex] != VK_NULL_HANDLE) @@ -32,6 +75,13 @@ // Shutdown calls this after vkDeviceWaitIdle and after all per-submit // descriptor pools/framebuffers/views have been collected. auto& cache = *m_wireColorBlitResources; + // Device idle here: cached blit targets no submission names any more. + for (const auto& entry : cache.cachedTargets) { + vkDestroyFramebuffer(m_device, entry.framebuffer, nullptr); + vkDestroyImageView(m_device, entry.srcView, nullptr); + vkDestroyImageView(m_device, entry.dstView, nullptr); + } + cache.cachedTargets.clear(); for (const auto& target : cache.targets) { vkDestroyPipeline(m_device, target.pipeline, nullptr); vkDestroyRenderPass(m_device, target.renderPass, nullptr); @@ -327,16 +377,62 @@ VK_VERIFY(vkCreateImageView(m_device, &view, nullptr, &handle)); return handle; }; - const VkImageView sourceView = makeView(source), destinationView = makeView(destination); - VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; - framebufferInfo.renderPass = target->renderPass; - framebufferInfo.attachmentCount = 1; - framebufferInfo.pAttachments = &destinationView; - framebufferInfo.width = destination.extent.width; - framebufferInfo.height = destination.extent.height; - framebufferInfo.layers = 1; - VkFramebuffer framebuffer = VK_NULL_HANDLE; - VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer)); + // P15: the views and framebuffer come from the cross-frame cache (see CachedTarget). + RetireWireColorBlitTargets(/*all=*/false); + WireColorBlitResources::CachedTarget wanted{}; + wanted.srcImage = source.image; + wanted.srcLevel = source.level; + wanted.srcLayer = source.layer; + wanted.srcFormat = source.format; + wanted.dstImage = destination.image; + wanted.dstLevel = destination.level; + wanted.dstLayer = destination.layer; + wanted.dstFormat = destination.format; + wanted.dstExtent = destination.extent; + wanted.renderPass = target->renderPass; + wanted.textureImageEpoch = m_textureManager ? m_textureManager->GetTextureImageEpoch() : 0; + wanted.resourceEraseEpoch = m_textureManager ? m_textureManager->GetResourceEraseEpoch() : 0; + wanted.dstIsDefault = destination.isDefault; + wanted.swapchain = destination.isDefault ? m_swapchainObject.GetHandle() : VK_NULL_HANDLE; + const auto sameTarget = [&](const WireColorBlitResources::CachedTarget& entry) { + return entry.srcImage == wanted.srcImage && entry.srcLevel == wanted.srcLevel && + entry.srcLayer == wanted.srcLayer && entry.srcFormat == wanted.srcFormat && + entry.dstImage == wanted.dstImage && entry.dstLevel == wanted.dstLevel && + entry.dstLayer == wanted.dstLayer && entry.dstFormat == wanted.dstFormat && + entry.dstExtent.width == wanted.dstExtent.width && + entry.dstExtent.height == wanted.dstExtent.height && entry.renderPass == wanted.renderPass && + entry.textureImageEpoch == wanted.textureImageEpoch && + entry.resourceEraseEpoch == wanted.resourceEraseEpoch && entry.swapchain == wanted.swapchain && + entry.dstIsDefault == wanted.dstIsDefault; + }; + auto& cachedTargets = cache.cachedTargets; + WireColorBlitResources::CachedTarget* hit = nullptr; + for (auto& entry : cachedTargets) + if (sameTarget(entry)) { hit = &entry; break; } + if (hit == nullptr) { + if (cachedTargets.size() >= WireColorBlitResources::kCachedTargets) { + WireRetiredObjects evicted{}; + evicted.framebuffers.push_back(cachedTargets.front().framebuffer); + evicted.imageViews.push_back(cachedTargets.front().srcView); + evicted.imageViews.push_back(cachedTargets.front().dstView); + RetireWireObjects(Move(evicted)); + cachedTargets.erase(cachedTargets.begin()); + } + wanted.srcView = makeView(source); + wanted.dstView = makeView(destination); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = target->renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &wanted.dstView; + framebufferInfo.width = destination.extent.width; + framebufferInfo.height = destination.extent.height; + framebufferInfo.layers = 1; + VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &wanted.framebuffer)); + cachedTargets.push_back(wanted); + hit = &cachedTargets.back(); + } + const VkImageView sourceView = hit->srcView; + const VkFramebuffer framebuffer = hit->framebuffer; // Each in-flight blit owns its descriptor set. Updating a cached set // would rewrite the source view of an older queued blit. VkDescriptorPool descriptorPool = VK_NULL_HANDLE; @@ -428,11 +524,11 @@ // This native pass binds its own set/layout without going through the // application's descriptor binder. Its next fast path must rebind. if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary(); - WireRetiredObjects retired{}; - retired.framebuffers.push_back(framebuffer); - retired.imageViews = {sourceView, destinationView}; - if (descriptorPool != VK_NULL_HANDLE) retired.descriptorPools.push_back(descriptorPool); - RetireWireObjects(Move(retired)); + if (descriptorPool != VK_NULL_HANDLE) { + WireRetiredObjects retired{}; + retired.descriptorPools.push_back(descriptorPool); + RetireWireObjects(Move(retired)); + } if (destination.isDefault) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true); return true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index ca8ecfdf..b877a484 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -201,9 +201,11 @@ void VulkanRenderer::ClearAllWireDrawPassCaches() { for (auto& entry : m_wireDrawPassCache) DestroyWireDrawPassCacheEntry(entry); m_wireDrawPassCache.clear(); - if (m_wireColorBlitResources) + if (m_wireColorBlitResources) { for (Uint32 frameIndex = 0; frameIndex < m_frameContext.GetFrameCount(); ++frameIndex) ResetWireColorBlitFramePool(frameIndex); + RetireWireColorBlitTargets(/*all=*/true); + } } void VulkanRenderer::CollectWireObjects(Uint64 completedSubmit, Bool all) {