From acaa9f6dc71e3a40dff7f7383244c343846ee9c5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 12 Jul 2026 10:11:49 -0400 Subject: [PATCH] [Perf] (MG_Backend/DirectVulkan): zero-copy UBO bind - point descriptor at the app's persistent VkBuffer instead of a per-draw transient copy; fps 127->166 --- .../DirectVulkan/Renderer/UniformManager.cpp | 68 ++++++++++++++----- .../DirectVulkan/Renderer/UniformManager.h | 12 +++- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 0b0e0afe..aa8fc7dc 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -578,9 +578,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, - const void*& outData, VkDeviceSize& outSize) const { - outData = nullptr; - outSize = 0; + UboBindResult& out) const { + const void* outData = nullptr; + VkDeviceSize outSize = 0; MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveUniformBufferPayload: GL context is null"); MOBILEGL_ASSERT(binding < programObj.bindingKinds.size(), @@ -596,6 +596,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { outData = emptyGlobalUbo.data(); outSize = static_cast(emptyGlobalUbo.size()); } + // The global UBO is CPU uniform data, not an app buffer -> always UploadTransient. + out.payload = outData; + out.payloadSize = outSize; return outData != nullptr && outSize > 0; } @@ -659,6 +662,26 @@ namespace MobileGL::MG_Backend::DirectVulkan { Memcpy(paddedUbo.data(), outData, static_cast(available)); outData = paddedUbo.data(); } + out.payload = outData; + out.payloadSize = outSize; + + // Zero-copy direct bind: for a persistent-mapped coherent app buffer whose full reflected + // block fits within the aligned bound range, point the descriptor straight at the app's + // resident VkBuffer (the same buffer the GLES backend binds) with the block range as the + // dynamic offset - no per-draw copy into a transient ring. Any gate failing keeps the + // UploadTransient payload above. AcquireResidentSlice does the persistent busy-tracking and + // (for the persistent case) hits a zero-work fast path returning the whole-buffer slice. + if (bufferObject->IsBackendPersistentMapped() && available >= blockSize && + (rangeStart % m_minDynamicOffsetAlignment) == 0) { + BufferSlice slice{}; + if (m_bufferManager->AcquireResidentSlice(BufferKind::Uniform, bufferObject, slice) && + slice.IsValid() && slice.offset == 0 && slice.size >= rangeStart + blockSize) { + out.directBindable = true; + out.buffer = slice.buffer; + out.range = blockSize; + out.dynamicOffset = rangeStart; + } + } return true; } @@ -850,31 +873,40 @@ namespace MobileGL::MG_Backend::DirectVulkan { write.descriptorCount = 1; if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) { - const void* payload = nullptr; - VkDeviceSize payloadSize = 0; - const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, payload, payloadSize); - MOBILEGL_ASSERT(hasPayload && payload != nullptr && payloadSize > 0, + UboBindResult ubo{}; + const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, ubo); + MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0, "UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u", binding); - BufferSlice slice{}; - if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, payload, payloadSize, - m_minDynamicOffsetAlignment, slice)) { - MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u", - binding); - return false; - } - VkDescriptorBufferInfo bufferInfo{}; - bufferInfo.buffer = slice.buffer; + // Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo + // is stable across draws and the descriptor-set reuse cache keeps hitting. bufferInfo.offset = 0; - bufferInfo.range = payloadSize; + Uint32 dynOffset; + if (ubo.directBindable) { + // Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy. + bufferInfo.buffer = ubo.buffer; + 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", + binding); + return false; + } + bufferInfo.buffer = slice.buffer; + bufferInfo.range = ubo.payloadSize; + dynOffset = static_cast(slice.offset); + } bufferInfos.push_back(bufferInfo); write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; write.pBufferInfo = &bufferInfos.back(); writes.push_back(write); - dynamicOffsets.push_back(static_cast(slice.offset)); + dynamicOffsets.push_back(dynOffset); } else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) { VkBufferView bufferView = VK_NULL_HANDLE; if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) || diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index f9e216c5..652b119b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -89,9 +89,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, VkDescriptorImageInfo& outImageInfo) const; + // Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident + // VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload. + struct UboBindResult { + Bool directBindable = false; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceSize range = 0; // reflected block size; constant across draws (hashed) + VkDeviceSize dynamicOffset = 0; // block range start; moves per draw (NOT hashed) + const void* payload = nullptr; // fallback UploadTransient path + VkDeviceSize payloadSize = 0; + }; Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, - const void*& outData, VkDeviceSize& outSize) const; + UboBindResult& out) const; Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const; Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex); VkResult AllocateDescriptorSetsFromActivePool(