From b253df881d5e32cd389590382b7aa15a8e8dedad Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 11 Jul 2026 22:06:28 -0400 Subject: [PATCH] [Perf] (DirectVulkan): memoize per-draw texture sync in SetupDraw Each sampled texture was resolved ~3x per draw: SetupDraw's layout-probe loop, its post-transition loop, and again inside ResolveSamplerDescriptor. No GL texture mutation happens mid-SetupDraw, and layout is tracked on the TextureResource independently of SyncTexture, so the repeat SyncTexture work (mip-completeness / resource+view resync / dirty scan) is pure redundancy. Add a per-draw memo in VkTextureManager (BeginDrawSyncScope/EndDrawSyncScope + RAII DrawSyncScope guard around SetupDraw): after the first successful sync of a texture in a draw, repeat SyncTextureAndGetDescriptor calls short-circuit to the already-synced resource. Device-verified on Adreno 830 (MC 26.3-snapshot3, Magma): rendering correct, no validation errors; wall-clock profile of the render thread shows SyncTextureAndGetDescriptor dropping from 15.2% to ~5% and SetupDraw from 43.7% to 28.9%. Co-Authored-By: Claude Opus 4.8 --- .../Renderer/VkTextureManager.cpp | 42 +++++++++++++++++++ .../DirectVulkan/Renderer/VkTextureManager.h | 28 +++++++++++++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 4 ++ 3 files changed, 74 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 69a2b049..322805b9 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -683,10 +683,39 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + void VkTextureManager::BeginDrawSyncScope() { + m_drawSyncedThisDraw.clear(); + m_drawSyncScopeActive = true; + } + + void VkTextureManager::EndDrawSyncScope() { + m_drawSyncScopeActive = false; + m_drawSyncedThisDraw.clear(); + } + VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) { MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE"); const TextureIdentity identity = MakeTextureIdentity(&texture); + + // Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully + // synced earlier in this draw cannot have changed since (no GL mutation runs + // mid-SetupDraw), so skip the heavy SyncTexture work and hand back the + // already-synced resource. Layout lives on the resource and is updated by the + // transition path, so the short-circuited resource still reflects the truth. + const Bool memoActive = m_drawSyncScopeActive; + if (memoActive) { + for (const TextureIdentity& synced : m_drawSyncedThisDraw) { + if (synced == identity) { + auto cachedIt = m_textureResources.find(identity); + if (cachedIt != m_textureResources.end() && cachedIt->second.image != VK_NULL_HANDLE) { + return &(cachedIt->second); + } + break; // resource unexpectedly gone -> fall through to a full sync + } + } + } + auto aliveIt = m_aliveObjects.find(identity); if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) { EraseTrackedTexture(aliveIt->first); @@ -717,6 +746,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { return nullptr; } + if (memoActive) { + Bool recorded = false; + for (const TextureIdentity& synced : m_drawSyncedThisDraw) { + if (synced == identity) { + recorded = true; + break; + } + } + if (!recorded) { + m_drawSyncedThisDraw.push_back(identity); + } + } + return &(it->second); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 1727481f..5aca9718 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -200,6 +200,30 @@ public: Uint32 layerCount = 1); SizeT CollectGarbage(); + + // Per-draw sync memo. Within a single SetupDraw the same sampled texture is + // resolved ~3x (SetupDraw's layout-probe loop, its post-transition loop, and + // again inside ResolveSamplerDescriptor). No GL texture mutation can happen + // mid-SetupDraw, and layout is tracked on the TextureResource independently of + // SyncTexture, so after the first successful sync of a texture in a draw the + // heavy SyncTexture work (mip-completeness/resource/view resync + dirty scan) + // is pure redundancy. BeginDrawSyncScope opens a window in which repeat + // SyncTextureAndGetDescriptor calls short-circuit to the already-synced + // resource; EndDrawSyncScope closes it. Use the RAII DrawSyncScope guard. + void BeginDrawSyncScope(); + void EndDrawSyncScope(); + + // RAII guard that opens/closes a per-draw sync memo window (see above). + class DrawSyncScope { + public: + explicit DrawSyncScope(VkTextureManager& manager) : m_manager(manager) { m_manager.BeginDrawSyncScope(); } + ~DrawSyncScope() { m_manager.EndDrawSyncScope(); } + DrawSyncScope(const DrawSyncScope&) = delete; + DrawSyncScope& operator=(const DrawSyncScope&) = delete; + private: + VkTextureManager& m_manager; + }; + private: Bool SyncTexture(MG_State::GLState::ITextureObject &texture, @@ -242,6 +266,10 @@ private: Uint32 m_currentFrameIndex = 0; Uint8 m_gcCounter = 0; + // Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of + // textures already fully synced in the current draw (small N -> flat scan). + Bool m_drawSyncScopeActive = false; + Vector m_drawSyncedThisDraw; std::unordered_map, TextureIdentityHash> m_aliveObjects; std::unordered_map m_textureResources; Vector> m_deferredReleases; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 2639a263..6f8535a0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3285,6 +3285,10 @@ void main() { Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView) { + // Sync each sampled texture at most once across this whole draw: the layout + // probe loop, the post-transition loop, and ResolveSamplerDescriptor would + // otherwise each re-run the full SyncTexture path on the same textures. + VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager); m_textureManager->CollectGarbage(); const auto& drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();