From 6b0c2a15ab8568f319d1d5f04fc543a865b4cc2e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 30 Jul 2026 08:18:21 -0400 Subject: [PATCH] [Perf] (DirectVulkan): reuse unchanged global-UBO slices and skip identical descriptor binds --- .../DirectVulkan/Renderer/UniformManager.cpp | 81 ++++++++++++++++--- .../DirectVulkan/Renderer/UniformManager.h | 32 ++++++++ 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index d1aabb2d..41b9398b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -18,6 +18,7 @@ #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Metrics/TextureMetrics.h" #include +#include #include #include #include @@ -204,6 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The frame's descriptor sets are recycled above, so last frame's reuse target // is gone: start the per-draw descriptor-reuse cache fresh this frame. m_hasLastDescriptor = false; + m_lastBindValid = false; // Re-fingerprint the bound sampler set fresh this frame so any GL object address // reuse cannot outlive a single frame (see SamplerResolveMemo). for (auto& memo : m_samplerResolveMemo) { @@ -1189,16 +1191,47 @@ namespace MobileGL::MG_Backend::DirectVulkan { bufferInfo.range = ubo.range; dynOffset = static_cast(ubo.dynamicOffset); } else { - BufferSlice slice{}; - if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload, - ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) { - MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u", - binding, element); - return false; + // Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged + // uniform bytes re-use the slice already uploaded this frame. + const Bool isGlobalUbo = + programObj.globalUboBinding == static_cast(binding) && element == 0; + const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial(); + const Uint64 uboProgramLifetimeId = program.GetLifetimeId(); + const Uint32 uboContentVersion = program.GetUBOContentVersion(); + Bool reusedSlice = false; + if (isGlobalUbo) { + for (const auto& memo : m_globalUboMemo) { + if (memo.buffer != VK_NULL_HANDLE && + memo.programLifetimeId == uboProgramLifetimeId && + memo.frameSerial == uboFrameSerial && + memo.uboContentVersion == uboContentVersion && + memo.range == static_cast(ubo.payloadSize)) { + bufferInfo.buffer = memo.buffer; + bufferInfo.range = memo.range; + dynOffset = static_cast(memo.offset); + reusedSlice = true; + break; + } + } + } + if (!reusedSlice) { + BufferSlice slice{}; + if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload, + ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) { + MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u", + binding, element); + return false; + } + bufferInfo.buffer = slice.buffer; + bufferInfo.range = ubo.payloadSize; + dynOffset = static_cast(slice.offset); + if (isGlobalUbo) { + m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{ + uboProgramLifetimeId, uboFrameSerial, uboContentVersion, + slice.buffer, slice.offset, static_cast(ubo.payloadSize)}; + m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize; + } } - bufferInfo.buffer = slice.buffer; - bufferInfo.range = ubo.payloadSize; - dynOffset = static_cast(slice.offset); } bufferInfos.push_back(bufferInfo); // Dynamic offsets are consumed in binding order, then array element order, @@ -1337,8 +1370,34 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_hasLastDescriptor = cacheable; } - vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1, - &descriptorSet, static_cast(dynamicOffsets.size()), dynamicOffsets.data()); + // Skip the driver call when this exact binding is already live on the + // command buffer (see the bind-dedup shadow in the header). + const Uint32 offsetCount = static_cast(dynamicOffsets.size()); + Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet && + m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint && + m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets; + if (identicalBind) { + for (Uint32 i = 0; i < offsetCount; ++i) { + if (m_lastBindOffsets[i] != dynamicOffsets[i]) { + identicalBind = false; + break; + } + } + } + if (!identicalBind) { + vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1, + &descriptorSet, offsetCount, dynamicOffsets.data()); + if (offsetCount <= kMaxShadowedDynamicOffsets) { + m_lastBindValid = true; + m_lastBindSet = descriptorSet; + m_lastBindLayout = programObj.pipelineLayout; + m_lastBindPoint = bindPoint; + m_lastBindOffsetCount = offsetCount; + std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets); + } else { + m_lastBindValid = false; + } + } return true; } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 3011b405..706c75be 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -39,6 +39,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { void Shutdown(); void BeginFrame(Uint32 frameIndex); + // A command buffer (re)began recording: descriptor bindings recorded into + // the previous buffer do not carry over, so drop the bind-dedup shadow. + void OnCommandBufferBoundary() { m_lastBindValid = false; } // A ProgramFactory eviction just destroyed this layout: purge every frame // slot's cached descriptor sets for it, so a recycled handle value can never // stale-hit sets written for the dead layout's bindings. The sets are @@ -185,6 +188,35 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 m_lastDescriptorSignature = 0; Bool m_hasLastDescriptor = false; + // vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform + // block resolve to the same set AND the same dynamic offsets, so the + // driver call can be skipped outright. Command-buffer-scope state; reset + // via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on + // layout+bind point, so a pipeline-layout switch always rebinds. + static constexpr Uint32 kMaxShadowedDynamicOffsets = 8; + Bool m_lastBindValid = false; + VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE; + VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE; + VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + Uint32 m_lastBindOffsetCount = 0; + Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {}; + + // Global-UBO transient-slice reuse: MC leaves the default uniform block + // untouched across long GUI/terrain runs, so the per-draw re-upload of + // the same bytes can reuse the slice uploaded earlier THIS frame (frame + // serial guards arena recycling; the content version guards writes). + struct GlobalUboSliceMemo { + Uint64 programLifetimeId = 0; + Uint64 frameSerial = 0; + Uint32 uboContentVersion = 0; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceSize offset = 0; + VkDeviceSize range = 0; + }; + static constexpr Uint32 kGlobalUboMemoSize = 4; + GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize]; + Uint32 m_globalUboMemoNext = 0; + // Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which // stays the source of truth: its key hashes all sampler+texture state, so two distinct // sampler objects with identical state still resolve to one VkSampler. This memo only