diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.cpp index f2650365..b9cafd44 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.cpp @@ -888,7 +888,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } BufferSlice slice{}; - if (!m_bufferManager->UploadTransient(TransientBufferKind::Uniform, frameIndex, payload, payloadSize, + if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, payload, payloadSize, m_minDynamicOffsetAlignment, slice)) { MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u", binding); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index a1690978..69f13c68 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -9,6 +9,12 @@ #include "VkBufferManager.h" namespace MobileGL::MG_Backend::DirectVulkan { + namespace { + constexpr Uint32 kResidentBufferGCInterval = 60; + constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags = + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + } // namespace + Bool VkBufferManager::Initialize(const VkBufferManagerInitInfo& initInfo) { Shutdown(); @@ -16,12 +22,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkBufferManager::Initialize requires non-zero frame count"); m_initInfo = initInfo; + m_deferredResidentReleases.resize(initInfo.frameCount); + m_currentFrameIndex = 0; return InitializeTransientArenas(); } void VkBufferManager::Shutdown() { m_transientUploadArena.Shutdown(); + DestroyResidentBuffers(); + DestroyDeferredResidentReleases(); m_initInfo = {}; + m_currentFrameIndex = 0; + m_residentGcTick = 0; } Bool VkBufferManager::RecreateTransientArenas(Uint32 frameCount) { @@ -30,14 +42,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_transientUploadArena.Shutdown(); m_initInfo.frameCount = frameCount; + DestroyDeferredResidentReleases(); + m_deferredResidentReleases.resize(frameCount); + m_currentFrameIndex = 0; return InitializeTransientArenas(); } void VkBufferManager::BeginFrame(Uint32 frameIndex) { + MOBILEGL_ASSERT(frameIndex < m_deferredResidentReleases.size(), + "VkBufferManager::BeginFrame frame index out of range"); + m_currentFrameIndex = frameIndex; + CollectDeferredResidentReleases(frameIndex); m_transientUploadArena.BeginFrame(frameIndex); } - Bool VkBufferManager::UploadTransient(TransientBufferKind kind, Uint32 frameIndex, const void* data, + Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) { (void)kind; return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice); @@ -55,4 +74,173 @@ namespace MobileGL::MG_Backend::DirectVulkan { .persistentlyMapped = m_initInfo.transientPersistentMapping, }); } + + Bool VkBufferManager::SyncResidentBuffer(BufferKind kind, + const SharedPtr& bufferObject, + BufferSlice& outSlice) { + const VkBufferUsageFlags requiredUsage = GetVkBufferUsage(kind); + MOBILEGL_ASSERT(requiredUsage != 0, + "VkBufferManager::SyncResidentBuffer only supports resident vertex/index buffers"); + MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::SyncResidentBuffer requires valid buffer object"); + CollectResidentGarbageIfNeeded(); + + const auto* bufferData = bufferObject->GetDataReadOnly().get(); + MOBILEGL_ASSERT(bufferData != nullptr, "VkBufferManager::SyncResidentBuffer requires frontend buffer data"); + + const VkDeviceSize bufferSize = static_cast(bufferObject->GetSize()); + if (bufferSize == 0) { + MGLOG_E("VkBufferManager::SyncResidentBuffer failed: buffer size is zero"); + return false; + } + + auto& entry = m_residentBuffers[bufferObject.get()]; + entry.aliveRef = bufferObject; + + const auto changeBits = bufferObject->GetChangeBits(); + const Bool needsRecreate = !entry.buffer.IsValid() || entry.size != bufferSize || + ((entry.usage & requiredUsage) != requiredUsage) || + (changeBits & BufferChangeBits::PreferReallocationBit); + if (needsRecreate) { + const VkBufferUsageFlags recreatedUsage = entry.usage | requiredUsage; + DeferResidentRelease(std::move(entry.buffer)); + const Bool created = entry.buffer.Create({ + .allocator = m_initInfo.allocator, + .size = bufferSize, + .usage = recreatedUsage, + .memoryUsage = VMA_MEMORY_USAGE_AUTO, + .allocationFlags = kResidentBufferAllocationFlags, + }); + if (!created || entry.buffer.Map() == nullptr) { + MGLOG_E("VkBufferManager::SyncResidentBuffer failed: unable to create resident buffer"); + entry.buffer.Destroy(); + entry.size = 0; + entry.usage = 0; + return false; + } + if (!entry.buffer.Upload(bufferData->data(), bufferSize, 0)) { + MGLOG_E("VkBufferManager::SyncResidentBuffer failed: initial upload failed"); + entry.buffer.Destroy(); + entry.size = 0; + entry.usage = 0; + return false; + } + entry.size = bufferSize; + entry.usage = recreatedUsage; + bufferObject->ClearDirty(); + outSlice = entry.buffer.GetSlice(0, bufferSize); + return true; + } + + if (changeBits & BufferChangeBits::DirtyBit) { + const auto& dirtyRanges = bufferObject->GetDirtyRanges(); + for (const auto& range : dirtyRanges) { + const VkDeviceSize rangeOffset = static_cast(range.start); + const VkDeviceSize rangeSize = static_cast(range.end - range.start); + if (rangeSize == 0) { + continue; + } + if (!entry.buffer.Upload(bufferData->data() + range.start, rangeSize, rangeOffset)) { + MGLOG_E("VkBufferManager::SyncResidentBuffer failed: dirty range upload failed"); + return false; + } + } + bufferObject->ClearDirty(); + } + + outSlice = entry.buffer.GetSlice(0, bufferSize); + return true; + } + + void VkBufferManager::DowngradeResidentBufferToTransient(const SharedPtr& bufferObject) { + if (bufferObject == nullptr) { + return; + } + + auto it = m_residentBuffers.find(bufferObject.get()); + if (it == m_residentBuffers.end()) { + return; + } + + DeferResidentRelease(std::move(it->second.buffer)); + m_residentBuffers.erase(it); + } + + void VkBufferManager::DeferResidentRelease(VkBufferObject&& buffer) { + if (!buffer.IsValid()) { + return; + } + + if (m_deferredResidentReleases.empty()) { + buffer.Destroy(); + return; + } + + MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredResidentReleases.size(), + "VkBufferManager::DeferResidentRelease current frame index out of range"); + m_deferredResidentReleases[m_currentFrameIndex].push_back(std::move(buffer)); + } + + void VkBufferManager::CollectDeferredResidentReleases(Uint32 frameIndex) { + MOBILEGL_ASSERT(frameIndex < m_deferredResidentReleases.size(), + "VkBufferManager::CollectDeferredResidentReleases frame index out of range"); + m_deferredResidentReleases[frameIndex].clear(); + } + + VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) { + switch (kind) { + case BufferKind::Vertex: + case BufferKind::Index: + // A GL buffer can be rebound between ARRAY_BUFFER and ELEMENT_ARRAY_BUFFER, + // and may even be used as both within the same draw setup. Keep resident + // vertex/index buffers compatible with both roles from the start so we + // never need to recreate a buffer after it has already been bound. + return VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; + case BufferKind::Uniform: + default: + return 0; + } + } + + void VkBufferManager::CollectResidentGarbageIfNeeded() { + ++m_residentGcTick; + if (m_residentGcTick < kResidentBufferGCInterval) { + return; + } + CollectResidentGarbageNow(); + m_residentGcTick = 0; + } + + void VkBufferManager::CollectResidentGarbageNow() { + Vector staleBuffers; + staleBuffers.reserve(m_residentBuffers.size()); + + for (const auto& [rawBuffer, entry] : m_residentBuffers) { + if (entry.aliveRef.expired()) { + staleBuffers.push_back(rawBuffer); + } + } + + for (const auto* rawBuffer : staleBuffers) { + auto it = m_residentBuffers.find(const_cast(rawBuffer)); + if (it == m_residentBuffers.end()) { + continue; + } + DeferResidentRelease(std::move(it->second.buffer)); + m_residentBuffers.erase(it); + } + } + + void VkBufferManager::DestroyDeferredResidentReleases() { + for (auto& deferredReleases : m_deferredResidentReleases) { + deferredReleases.clear(); + } + m_deferredResidentReleases.clear(); + } + + void VkBufferManager::DestroyResidentBuffers() { + for (auto& [_, entry] : m_residentBuffers) { + entry.buffer.Destroy(); + } + m_residentBuffers.clear(); + } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 83042ae4..096cdb90 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -9,12 +9,13 @@ #pragma once #include "BufferArena.h" +#include "MG_State/GLState/BufferState/BufferObject.h" #include "../VkIncludes.h" #include #include namespace MobileGL::MG_Backend::DirectVulkan { - enum class TransientBufferKind : Uint8 { + enum class BufferKind : Uint8 { Vertex, Index, Uniform, @@ -38,13 +39,34 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool RecreateTransientArenas(Uint32 frameCount); void BeginFrame(Uint32 frameIndex); - Bool UploadTransient(TransientBufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size, + Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice); + Bool SyncResidentBuffer(BufferKind kind, const SharedPtr& bufferObject, + BufferSlice& outSlice); + void DowngradeResidentBufferToTransient(const SharedPtr& bufferObject); private: + struct ResidentBufferEntry { + WeakPtr aliveRef; + VkBufferObject buffer; + VkDeviceSize size = 0; + VkBufferUsageFlags usage = 0; + }; + Bool InitializeTransientArenas(); + static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); + void DeferResidentRelease(VkBufferObject&& buffer); + void CollectDeferredResidentReleases(Uint32 frameIndex); + void CollectResidentGarbageIfNeeded(); + void CollectResidentGarbageNow(); + void DestroyDeferredResidentReleases(); + void DestroyResidentBuffers(); VkBufferManagerInitInfo m_initInfo{}; BufferArena m_transientUploadArena; + UnorderedMap m_residentBuffers; + Vector> m_deferredResidentReleases; + Uint32 m_currentFrameIndex = 0; + Uint32 m_residentGcTick = 0; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 5eb46ba6..dd549cf5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -19,6 +19,29 @@ #include namespace MobileGL::MG_Backend::DirectVulkan { + static Bool ShouldUseTransientVertexIndexBuffer(const MG_State::GLState::BufferObject& bufferObject) { + switch (bufferObject.GetUsage()) { + case BufferUsage::StreamDraw: + case BufferUsage::StreamRead: + case BufferUsage::StreamCopy: + case BufferUsage::DynamicDraw: + case BufferUsage::DynamicRead: + case BufferUsage::DynamicCopy: + return true; + case BufferUsage::StaticDraw: + case BufferUsage::StaticRead: + case BufferUsage::StaticCopy: + default: + return false; + } + } + + static Bool HasTransientVertexIndexBufferThisFrame( + const Vector& buffers, + const MG_State::GLState::BufferObject* buffer) { + return std::find(buffers.begin(), buffers.end(), buffer) != buffers.end(); + } + static const char* VkImageLayoutToString(VkImageLayout layout) { switch (layout) { case VK_IMAGE_LAYOUT_UNDEFINED: @@ -340,6 +363,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VK_VERIFY(m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired), "Initialize, WaitAndAcquireNextImage"); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); + m_transientVertexIndexBuffersThisFrame.clear(); MGLOG_D("VulkanRenderer initialized"); } @@ -360,6 +384,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } m_vertexInputStateFactory.reset(); m_bufferManager.Shutdown(); + m_transientVertexIndexBuffersThisFrame.clear(); m_frameContext.Destroy(m_device, m_commandPool); @@ -412,7 +437,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector vkBuffers(bindingCount, VK_NULL_HANDLE); Vector vkOffsets(bindingCount, 0); - const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex(); auto findBufferByKey = [&](SizeT bufferKey) -> const MG_State::GLState::BufferObject* { const auto& attrs = vao.GetAllAttributes(); @@ -433,15 +457,32 @@ namespace MobileGL::MG_Backend::DirectVulkan { const SizeT bufferKey = vertexInputState.bindingBufferKeys[binding]; const MG_State::GLState::BufferObject* sourceBuffer = findBufferByKey(bufferKey); MOBILEGL_ASSERT(sourceBuffer != nullptr, "UploadAndBindVertexStreams failed to resolve source buffer"); - - const auto sourceData = sourceBuffer->GetDataReadOnly(); - - const SizeT sourceSize = sourceBuffer->GetSize(); + auto sourceBufferShared = MG_State::pGLContext->GetBufferObject(sourceBuffer->GetExternalIndex()); + MOBILEGL_ASSERT(sourceBufferShared != nullptr, + "UploadAndBindVertexStreams failed to resolve shared source buffer"); BufferSlice slice{}; - if (!m_bufferManager.UploadTransient(TransientBufferKind::Vertex, frameIndex, sourceData->data(), - static_cast(sourceSize), 16, slice)) { - MGLOG_E("UploadAndBindVertexStreams skipped: failed to upload binding %zu", binding); - return false; + const Bool transientThisFrame = + HasTransientVertexIndexBufferThisFrame(m_transientVertexIndexBuffersThisFrame, sourceBufferShared.get()); + const Bool isDirty = (sourceBufferShared->GetChangeBits() & BufferChangeBits::DirtyBit); + if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared) || transientThisFrame || isDirty) { + const auto sourceData = sourceBufferShared->GetDataReadOnly(); + const SizeT sourceSize = sourceBufferShared->GetSize(); + if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), + sourceData->data(), static_cast(sourceSize), 16, + slice)) { + MGLOG_E("UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding); + return false; + } + if (!transientThisFrame) { + m_transientVertexIndexBuffersThisFrame.push_back(sourceBufferShared.get()); + } + m_bufferManager.DowngradeResidentBufferToTransient(sourceBufferShared); + sourceBufferShared->ClearDirty(); + } else { + if (!m_bufferManager.SyncResidentBuffer(BufferKind::Vertex, sourceBufferShared, slice)) { + MGLOG_E("UploadAndBindVertexStreams skipped: failed to sync resident binding %zu", binding); + return false; + } } vkBuffers[binding] = slice.buffer; vkOffsets[binding] = slice.offset; @@ -474,24 +515,40 @@ namespace MobileGL::MG_Backend::DirectVulkan { const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get(); MOBILEGL_ASSERT(indexBuffer != nullptr, "UploadAndBindIndexBuffer requires bound EBO"); - const auto indexData = indexBuffer->GetDataReadOnly(); - MOBILEGL_ASSERT(indexData != nullptr && !indexData->empty(), "DrawElements requires non-empty EBO data"); - const SizeT indexSize = MG_Util::GetGLTypeSize(pIndexBufferView->indexType); const SizeT indexDataSizeBytes = pIndexBufferView->indexByteSize; MOBILEGL_ASSERT(pIndexBufferView->indexByteOffset + indexDataSizeBytes <= indexBuffer->GetSize(), "DrawElements index range out of bounds"); - const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex(); - const VkDeviceSize alignment = indexSize; BufferSlice slice{}; - if (!m_bufferManager.UploadTransient(TransientBufferKind::Index, frameIndex, - indexData->data() + pIndexBufferView->indexByteOffset, - static_cast(indexDataSizeBytes), alignment, slice)) { - MGLOG_E("DrawElements skipped: failed to prepare index upload buffer"); + auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex()); + MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO"); + const Bool transientThisFrame = + HasTransientVertexIndexBufferThisFrame(m_transientVertexIndexBuffersThisFrame, indexBufferShared.get()); + const Bool isDirty = (indexBufferShared->GetChangeBits() & BufferChangeBits::DirtyBit); + if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared) || transientThisFrame || isDirty) { + const auto indexData = indexBufferShared->GetDataReadOnly(); + MOBILEGL_ASSERT(indexData != nullptr && !indexData->empty(), "DrawElements requires non-empty EBO data"); + if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), + indexData->data() + pIndexBufferView->indexByteOffset, + static_cast(indexDataSizeBytes), indexSize, slice)) { + MGLOG_E("DrawElements skipped: failed to prepare transient index buffer"); + return false; + } + if (!transientThisFrame) { + m_transientVertexIndexBuffersThisFrame.push_back(indexBufferShared.get()); + } + m_bufferManager.DowngradeResidentBufferToTransient(indexBufferShared); + indexBufferShared->ClearDirty(); + vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, slice.offset, vkIndexType); + return true; + } + if (!m_bufferManager.SyncResidentBuffer(BufferKind::Index, indexBufferShared, slice)) { + MGLOG_E("DrawElements skipped: failed to sync resident index buffer"); return false; } - vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, slice.offset, vkIndexType); + vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, + slice.offset + static_cast(pIndexBufferView->indexByteOffset), vkIndexType); return true; } @@ -1270,6 +1327,7 @@ void main() { } VK_VERIFY(result, "Present, vkAcquireNextImageKHR"); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); + m_transientVertexIndexBuffersThisFrame.clear(); } void VulkanRenderer::CreateInstance() { @@ -1849,6 +1907,7 @@ void main() { MOBILEGL_ASSERT(okArena, "RecreateSwapchain: buffer manager transient arena initialization failed"); if (m_frameContext.GetFrameCount() > 0) { m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); + m_transientVertexIndexBuffersThisFrame.clear(); } } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 5f28f0aa..f36a17e3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -173,6 +173,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkCommandPool m_commandPool = VK_NULL_HANDLE; VkBufferManager m_bufferManager; + Vector m_transientVertexIndexBuffersThisFrame; Uint m_imageIndexAcquired = 0; FrameContext m_frameContext;