From 421c20984edd73f45e3edeee7bd93e3bf796e4cf Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 30 Jul 2026 02:40:47 -0400 Subject: [PATCH] [Fix] (DirectVulkan): stop loading and carrying dead default-framebuffer content - EGL swap semantics make the presented colour buffer's content undefined at its next acquire (EGL_BUFFER_DESTROYED, the implementation default) and every ancillary depth/stencil buffer's content undefined after ANY swap, yet the default-FBO render pass reloaded both with LOAD_OP_LOAD every frame; SwapchainObject now tracks per-image content validity (defined when a pass stores into the attachment, invalidated at present) and the render-pass manager turns an undefined attachment's tile load into LOAD_OP_DONT_CARE with initialLayout=UNDEFINED, keyed into both hashes so the cached LOAD variants cannot be hit by mistake - the default framebuffer's depth attachment is now attached ON DEMAND: a draw with depth test and stencil test both disabled (GL: a disabled test neither reads nor writes its buffer), and no pending depth/stencil clear, resolves to a depth-less pass flavour, dropping the D24S8 tile load AND store outright - MC 26.2 renders its GUI into its own FBO and only ever blits colour to the default framebuffer, so its swapchain pass carried a full-screen depth round-trip for nothing - the flavour only escalates: an active depth-full pass absorbs depth-less draws unchanged, while a depth-using draw against a depth-less pass resolves to an incompatible entry and splits, its depth loading DONT_CARE (the content was undefined all along); the depth-less flavour is folded into ComputeHash and the per-draw fast-path memo so the two flavours can never alias --- .../DirectVulkan/Renderer/SwapchainObject.cpp | 33 ++++++++ .../DirectVulkan/Renderer/SwapchainObject.h | 17 ++++ .../Renderer/VkRenderPassManager.cpp | 81 +++++++++++++++++-- .../Renderer/VkRenderPassManager.h | 22 ++++- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 24 +++++- 5 files changed, 166 insertions(+), 11 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp index 2ba3c10c..0f5dc4d3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp @@ -262,6 +262,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_images.resize(imageCount, VK_NULL_HANDLE); VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data())); m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED); + // Fresh swapchain images hold garbage until a render pass stores into them. + m_imageContentDefined.assign(imageCount, false); + m_depthStencilContentDefined.assign(imageCount, false); CreateImageViews(device); CreateDepthStencilResources(device, physicalDevice); @@ -433,9 +436,39 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_images.clear(); m_imageLayouts.clear(); + m_imageContentDefined.clear(); + m_depthStencilContentDefined.clear(); m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; } + Bool SwapchainObject::IsImageContentDefined(Uint32 index) const { + MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range"); + return m_imageContentDefined[index]; + } + + void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) { + MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range"); + m_imageContentDefined[index] = defined; + } + + Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const { + MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(), + "Swapchain depth/stencil content index out of range"); + return m_depthStencilContentDefined[index]; + } + + void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) { + MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(), + "Swapchain depth/stencil content index out of range"); + m_depthStencilContentDefined[index] = defined; + } + + void SwapchainObject::SetAllDepthStencilContentUndefined() { + for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) { + m_depthStencilContentDefined[i] = false; + } + } + VkImage SwapchainObject::GetImage(Uint32 index) const { MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range"); return m_images[index]; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h index 968027a6..c22f07bd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h @@ -52,6 +52,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { void SetImageLayout(Uint32 index, VkImageLayout layout); SizeT GetImageCount() const { return m_images.size(); } + // EGL content-validity tracking for the default framebuffer. A color + // buffer's content is undefined once its image has been presented + // (EGL_BUFFER_DESTROYED swap behaviour, the implementation default), + // and every ancillary (depth/stencil) buffer's content is undefined + // after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The + // render-pass manager turns an undefined attachment's tile load into + // LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds + // garbage) and a render pass storing into an attachment sets it back + // to defined. + Bool IsImageContentDefined(Uint32 index) const; + void SetImageContentDefined(Uint32 index, Bool defined); + Bool IsDepthStencilContentDefined(Uint32 index) const; + void SetDepthStencilContentDefined(Uint32 index, Bool defined); + void SetAllDepthStencilContentUndefined(); + private: void CreateImageViews(VkDevice device); void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice); @@ -77,5 +92,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector m_depthStencilImageMemories; Vector m_depthStencilImageViews; Vector m_depthStencilImageLayouts; + Vector m_imageContentDefined; + Vector m_depthStencilContentDefined; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 1a97ff3d..86a2726e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -481,7 +481,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { } VkRenderPassManager::HashType VkRenderPassManager::ComputeHash( - const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear) { + const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear, + Bool includeDefaultFboDepthStencil) { XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); if (isDefaultFbo) { @@ -560,9 +561,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { attachment <= FramebufferAttachmentType::BackRight); if (isDefaultColorAttachment) { currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex); + // Content validity feeds the attachment's loadOp (see the + // creation path), so it must key the cache as well. + if (!m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) { + currentLayout = VK_IMAGE_LAYOUT_UNDEFINED; + } } else if (attachment == FramebufferAttachmentType::Depth || attachment == FramebufferAttachmentType::Stencil) { currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex); + if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) { + currentLayout = VK_IMAGE_LAYOUT_UNDEFINED; + } } } else { auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture); @@ -617,14 +626,49 @@ namespace MobileGL::MG_Backend::DirectVulkan { combineFramebufferAttachmentObjHash(drawbuf); } - combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth); - combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil); + // The depth-less default-FBO flavor omits the depth/stencil attachment + // entirely, so it must hash differently from the depth-full flavor. + const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil; + XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded))); + if (depthStencilIncluded) { + combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth); + combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil); + } return XXH64_digest(m_hashState); } RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, - Uint32 swapchainImageIndex) { + Uint32 swapchainImageIndex, + Bool drawUsesDepthStencil) { + // Resolve the default-FBO depth flavor (see the header comment): keep the + // depth attachment when the caller needs it, when a depth/stencil clear is + // pending, or when the active pass already carries it (escalate-only, so + // alternating depth-less draws never split an established depth pass). + Bool includeDefaultFboDepthStencil = true; + if (fbo.IsDefaultFramebuffer()) { + Bool activeDefaultHasDepthStencil = false; + if (const auto* active = GetActiveRenderPass()) { + Bool activeIsSwapchainPass = false; + Bool activeHasSwapchainDepthStencil = false; + for (const auto& tracked : active->trackedAttachmentLayouts) { + activeIsSwapchainPass |= tracked.target == TrackedAttachmentTarget::SwapchainColor; + activeHasSwapchainDepthStencil |= + tracked.target == TrackedAttachmentTarget::SwapchainDepthStencil; + } + activeDefaultHasDepthStencil = activeIsSwapchainPass && activeHasSwapchainDepthStencil; + } + const auto& defaultDepthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth); + const auto& defaultStencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil); + const Bool pendingDepthStencilClear = + (defaultDepthAtt.IsTexture() && m_clearManager.HasPendingClear(defaultDepthAtt)) || + HasPendingRenderbufferClear(defaultDepthAtt) || + (defaultStencilAtt.IsTexture() && m_clearManager.HasPendingClear(defaultStencilAtt)) || + HasPendingRenderbufferClear(defaultStencilAtt); + includeDefaultFboDepthStencil = + drawUsesDepthStencil || activeDefaultHasDepthStencil || pendingDepthStencilClear; + } + auto hasPendingClearOnFramebuffer = [&]() -> Bool { const auto& drawBuffers = fbo.GetDrawBuffers(); for (auto attachment : drawBuffers) { @@ -674,6 +718,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex && m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() && m_rpFastRbEpoch == m_renderbufferImageEpoch && + (!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) && m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) { auto activeIt = m_renderPasses.find(activeRenderPass->hash); if (activeIt != m_renderPasses.end()) { @@ -682,7 +727,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false); + auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false, includeDefaultFboDepthStencil); if (activeRenderPass != nullptr && activeRenderPass->CompatibleWith(compatibilityHash) && !hasPendingClearOnFramebuffer()) { @@ -699,10 +744,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch(); m_rpFastRbEpoch = m_renderbufferImageEpoch; m_rpFastRenderPassHash = activeRenderPass->hash; + m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment; activeIt->second.lastUsedFrame = m_frameCounter; return activeIt->second; } - auto hash = ComputeHash(fbo, swapchainImageIndex, true); + auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil); auto it = m_renderPasses.find(hash); if (it != m_renderPasses.end()) { it->second.lastUsedFrame = m_frameCounter; @@ -894,6 +940,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(), "GetOrCreateRenderPass: swapchain image index out of range"); trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex); + // EGL: a presented color buffer's content is undefined when its + // image comes back around (EGL_BUFFER_DESTROYED, the default + // swap behaviour) - skip the tile load instead of reloading + // stale pixels nobody may rely on. + if (!hasClear && !m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) { + trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED; + } trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo { .target = TrackedAttachmentTarget::SwapchainColor, .swapchainImageIndex = swapchainImageIndex, @@ -976,6 +1029,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt : (isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr); + // Depth-less default-FBO flavor: nothing in this pass touches depth/stencil + // and their content is undefined anyway (EGL swap), so drop the attachment + // and its whole tile load + store. + if (isDefaultFbo && !includeDefaultFboDepthStencil) { + selectedDepthStencilAttachment = nullptr; + } const Bool hasDistinctDepthAndStencilAttachments = isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) && !sameDepthStencilAttachmentObject(depthAtt, stencilAtt); @@ -994,6 +1053,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkImageLayout trackedDepthLayout = isDefaultFbo ? m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + // EGL 1.5 §3.10.1: every ancillary (depth/stencil) buffer's content is + // undefined after a swap, so the first default-FBO pass of a frame can + // skip the depth/stencil tile load outright. + if (isDefaultFbo && !m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) { + trackedDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED; + } depthAttachmentDescription.flags = 0; VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT; Int depthAttachmentId = 0; @@ -1382,11 +1447,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { case TrackedAttachmentTarget::SwapchainColor: MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null"); s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout); + // The pass stored into the attachment: its content is defined + // until the image is next presented. + s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true); break; case TrackedAttachmentTarget::SwapchainDepthStencil: MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null"); s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout); + s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true); break; default: MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d", diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index dfd5f132..eff34a4e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -188,8 +188,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { HashType ComputeHash( const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, - Bool includePendingClear = true); - RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex); + Bool includePendingClear = true, + Bool includeDefaultFboDepthStencil = true); + // drawUsesDepthStencil: whether the operation about to run inside the pass + // reads or writes the depth/stencil buffer (depth test or stencil test + // enabled, or a depth/stencil clear). Only consulted for the DEFAULT + // framebuffer: EGL undefines its ancillary buffers at every swap, so a + // default-FBO pass whose draws provably never touch depth/stencil is + // created WITHOUT the depth attachment - on a tiler that skips the whole + // depth tile load AND store. The flavor only escalates: once a pass with + // depth is active, later depth-less draws keep using it, and a depth-using + // draw against a depth-less active pass resolves to a new (incompatible) + // entry, which the caller's compatibility check turns into a pass split; + // the new pass's depth loads DONT_CARE (content was undefined all along). + RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, + Uint32 swapchainImageIndex, + Bool drawUsesDepthStencil = true); void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload, const MG_State::GLState::FramebufferObject& drawFbo); void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload, @@ -231,6 +245,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 m_rpFastTexEpoch = 0; Uint64 m_rpFastRbEpoch = 0; Uint64 m_rpFastRenderPassHash = 0; + // Whether the memoized entry carries a depth/stencil attachment; a + // default-FBO resolution whose effective depth request differs must + // miss the memo (the depth-less/depth-full flavors hash differently). + Bool m_rpFastHadDepthStencil = false; public: struct RenderbufferResource { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 4cce10f7..b0ba9659 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -4427,11 +4427,19 @@ void main() { static_cast(transitionedResource->layout)); } - auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired); + // Depth/stencil participation of THIS draw, for the default-FBO depth-less + // pass flavor (GL: a disabled depth/stencil test neither reads nor writes + // its buffer). + const Bool drawUsesDepthStencil = + MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) || + MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest); + auto* renderPassEntry = + &m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil); if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); activeRenderPass = nullptr; - renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired); + renderPassEntry = + &m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil); } if (renderPassEntry->attachmentCount == 0 || renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { MGLOG_D("SetupDraw skipped: drawFbo=%u resolved to an empty render pass (attachmentCount=%u extent=%dx%d)", @@ -5453,7 +5461,10 @@ void main() { "TryBlitToDefaultFramebufferWithShader: failed to create sampled view for textureId=%d mip=%u", sourceTexture->GetExternalIndex(), srcBinding.mipLevel); - auto& renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(drawFbo, m_imageIndexAcquired); + // A color-only blit never touches depth/stencil: let the default-FBO pass + // it opens skip the depth attachment (depth-less flavor). + auto& renderPassEntry = + m_renderPassManager->GetOrCreateRenderPass(drawFbo, m_imageIndexAcquired, /*drawUsesDepthStencil=*/false); const Bool ok = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, renderPassEntry); MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__); @@ -7696,6 +7707,13 @@ void main() { result = VK_SUCCESS; } VK_VERIFY(result, "Present, vkQueuePresentKHR"); + // EGL swap semantics: the presented color buffer's content is undefined the + // next time this image is acquired (EGL_BUFFER_DESTROYED, the default swap + // behaviour), and EVERY ancillary depth/stencil buffer's content is + // undefined after any swap. The render-pass manager turns the undefined + // attachments' next tile loads into LOAD_OP_DONT_CARE. + m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, false); + m_swapchainObject.SetAllDepthStencilContentUndefined(); // The authoritative check, done here - after the frame is presented, before the next // acquire. This is what makes a launcher-side resolution change take effect: shrinking // the window's buffer (SurfaceHolder.setFixedSize) moves currentExtent, the swapchain