From 009e37ec6f73eae5fc8be9a4e7d4b09b5c680057 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 12 Jul 2026 01:52:32 -0400 Subject: [PATCH] [Perf] (DirectVulkan): reuse descriptor set across draws with identical bindings BindProgramUniformBuffers rebuilt a fresh descriptor set and called vkUpdateDescriptorSets on every draw, even when consecutive draws bound the exact same textures/samplers/buffers (common in MC: many draws share a program + atlas). Now, after resolving the bindings (still needed for the UBO dynamic offset), compute a cheap word-wise signature of the resolved descriptor content + layout; when it matches the previous draw, reuse that descriptor set and skip AcquireDescriptorSet + vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Correct by construction: bindings are re-resolved every draw so the signature always reflects current state and reuse only happens on an exact match; the reused set is never re-acquired within a frame (the acquire cursor only advances); the descriptor set layout is in the signature so reuse never crosses programs; the cache resets each frame in BeginFrame when the frame's sets are recycled; sampler overrides (blits) bypass and invalidate it. The signature hashes 64-bit words (the Vk*Info payloads are 8-byte-multiple sized and value-initialized) so its own per-draw cost stays small. Device-verified on Adreno 830 (MC 26.3-snapshot3, optimized -O2 Magma): rendering correct, no validation errors. Render-thread wall-clock profile: BindProgramUniformBuffers 22.85% -> 19.82% (vkUpdateDescriptorSets ~5% dropped below noise; word-wise signature adds ~0.6% self), SetupDraw 62% -> 60%. --- .../DirectVulkan/Renderer/UniformManager.cpp | 70 ++++++++++++++++--- .../DirectVulkan/Renderer/UniformManager.h | 9 +++ 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 8779c7ce..0b0e0afe 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -193,6 +193,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) { cacheEntryPair.second.cursor = 0; } + // 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; } Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, @@ -806,13 +809,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { frame.activeDescriptorPoolIndex = 0; } + // The descriptor set is chosen AFTER the writes are built (below), so a draw + // whose resolved descriptor content matches the previous draw can reuse that + // set and skip both AcquireDescriptorSet and vkUpdateDescriptorSets. VkDescriptorSet descriptorSet = VK_NULL_HANDLE; - VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet); - if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) { - MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d", - allocResult); - return false; - } MOBILEGL_ASSERT(m_textureManager != nullptr, "BindProgramUniformBuffers: texture manager is null"); MOBILEGL_ASSERT(m_samplerManager != nullptr, "BindProgramUniformBuffers: sampler manager is null"); @@ -944,8 +944,62 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - if (!writes.empty()) { - vkUpdateDescriptorSets(m_device, static_cast(writes.size()), writes.data(), 0, nullptr); + // Reuse the previous draw's descriptor set when the resolved content is + // byte-identical (only the bind-time dynamic offsets differ). The signature + // covers the descriptor-set layout + every write's binding/type/count + the + // pointed-to buffer/image/texel-buffer infos (all value-initialized, so no + // padding noise). Correctness: bindings are re-resolved every draw, so the + // signature always reflects the current state and reuse happens only on an + // exact match; the reused set is never re-acquired within a frame (the acquire + // cursor only advances), so its written contents survive; the layout is part of + // the signature so reuse never crosses programs. Sampler overrides (blits) + // bypass and invalidate the cache. + const Bool cacheable = (samplerBindingOverride == nullptr); + Uint64 signature = 0xcbf29ce484222325ULL; + { + const auto mix64 = [&signature](Uint64 word) { + signature = (signature ^ word) * 0x100000001b3ULL; + }; + // The hashed descriptor payloads (VkDescriptorBufferInfo=24B, + // VkDescriptorImageInfo=24B, VkBufferView=8B) are all 8-byte-multiple sized + // and value-initialized (padding is zero), so hashing 64-bit words at a time + // is exact and ~8x cheaper than byte-wise - the signature is recomputed every + // draw, so its own cost has to stay small. + const auto mixWords = [&mix64](const void* data, SizeT byteSize) { + const auto* words = static_cast(data); + for (SizeT i = 0; i < byteSize / sizeof(Uint64); ++i) { + mix64(words[i]); + } + }; + mixWords(&programObj.descriptorSetLayout, sizeof(programObj.descriptorSetLayout)); + for (const auto& write : writes) { + mix64((static_cast(write.dstBinding) << 40) ^ + (static_cast(write.descriptorType) << 8) ^ + static_cast(write.descriptorCount)); + } + mixWords(bufferInfos.data(), bufferInfos.size() * sizeof(VkDescriptorBufferInfo)); + mixWords(imageInfos.data(), imageInfos.size() * sizeof(VkDescriptorImageInfo)); + mixWords(texelBufferViews.data(), texelBufferViews.size() * sizeof(VkBufferView)); + } + + if (cacheable && m_hasLastDescriptor && signature == m_lastDescriptorSignature) { + descriptorSet = m_lastBoundDescriptorSet; + } else { + VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet); + if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) { + MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d", + allocResult); + return false; + } + for (auto& write : writes) { + write.dstSet = descriptorSet; + } + if (!writes.empty()) { + vkUpdateDescriptorSets(m_device, static_cast(writes.size()), writes.data(), 0, nullptr); + } + m_lastBoundDescriptorSet = descriptorSet; + m_lastDescriptorSignature = signature; + m_hasLastDescriptor = cacheable; } vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index d1699968..f9e216c5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -121,6 +121,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector m_imageInfosScratch; Vector m_texelBufferViewsScratch; Vector m_dynamicOffsetsScratch; + + // Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers). + // When a draw's resolved descriptor content is byte-identical to the previous + // draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet + + // vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each + // frame in BeginFrame because the frame's descriptor sets are recycled there. + VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE; + Uint64 m_lastDescriptorSignature = 0; + Bool m_hasLastDescriptor = false; }; } // namespace MobileGL::MG_Backend::DirectVulkan