// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
//   https://www.gnu.org/licenses/gpl-3.0.txt
//   https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header

#include "VkBufferManager.h"
#include "../DirectVulkan.h"
#include "VulkanRenderer.h"

namespace MobileGL::MG_Backend::DirectVulkan {
    namespace {
        constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
            VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
        constexpr SizeT kLiveResourcePruneThreshold = 256;

        // See VkBufferManager::AcquireUnboundStorageDescriptor. 256 bytes: comfortably past
        // every minStorageBufferOffsetAlignment in the wild, and free.
        constexpr VkDeviceSize kUnboundStorageDescriptorBytes = 256;
        // See VkBufferManager::AcquireUnboundTexelBufferDescriptor. The same 256 bytes, for the
        // same reason plus one: a texel buffer view's range must be a whole number of texels of
        // whatever format the placeholder is asked for, and 256 divides by every texel size in
        // the GL image-format table (1, 2, 4, 8 and 16 bytes).
        constexpr VkDeviceSize kUnboundTexelBufferDescriptorBytes = 256;

        // A zero-copy persistent buffer is created once and never recreated (the app holds
        // its mapped pointer), and may be bound to any role, so it carries every usage.
        // TRANSFER_DST is added by CreateResidentStorage.
        constexpr VkBufferUsageFlags kPersistentBackedUsage =
            VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
            VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
            VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
            // "Every usage" has to mean every usage: a buffer texture reached through an IMAGE
            // unit takes a VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER descriptor, and the write is
            // invalid unless the buffer was created with this bit. Nothing asked for it until
            // imageBuffer support existed, so the omission was invisible.
            VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
        // Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
        // (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
        constexpr VkBufferUsageFlags kTransformFeedbackUsage =
            VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
        // The app writes into the persistent map with no explicit flush, so its memory must
        // be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
        constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;

        using MG_State::GLState::BackendBufferResource;
        using MG_State::GLState::BufferBackendOps;
        using MG_State::GLState::BufferObject;

        // The manager owned by the active VulkanRenderer; immediate ops route here.
        VkBufferManager* g_activeBufferManager = nullptr;

        void Ops_Respecify(BufferObject& bufferObject) {
            if (g_activeBufferManager) {
                g_activeBufferManager->OnRespecify(bufferObject);
            }
        }

        void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) {
            if (g_activeBufferManager) {
                g_activeBufferManager->OnSubData(bufferObject, offset, size);
            }
        }

        void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
                                  Flags<BufferMappingAccessBit> appAccess) {
            if (g_activeBufferManager) {
                g_activeBufferManager->OnFlushMappedRange(bufferObject, range, appAccess);
            }
        }

        // The CPU is about to read a buffer a shader wrote. Its bytes live in coherent
        // host-visible GPU storage (EnsureGpuResidentStorage adopts it when the buffer is
        // bound as a shader storage buffer), so nothing needs copying - but coherence only
        // says the writes are visible once they have happened, so the work has to retire
        // first.
        void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
            (void)bufferObject;
            if (pVulkanRenderer) {
                pVulkanRenderer->FinishPendingGpuWork();
            }
        }

        void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
            if (g_activeBufferManager) {
                return g_activeBufferManager->AcquirePersistentMap(bufferObject);
            }
            return nullptr;
        }

        void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
            if (g_activeBufferManager) {
                g_activeBufferManager->OnResourceDestroyed(std::move(resource));
            }
            // No active manager: the device/allocator is gone or going away and
            // Shutdown() already destroyed the storage; dropping the handle here
            // must not touch Vulkan. VkBufferResource's dtor destroys via VMA only
            // when the allocation is still valid, which Shutdown() cleared.
        }

        const BufferBackendOps g_vulkanBufferBackendOps = {
            .Respecify = Ops_Respecify,
            .SubData = Ops_SubData,
            .FlushMappedRange = Ops_FlushMappedRange,
            .OnDestroy = Ops_OnDestroy,
            .AcquirePersistentMap = Ops_AcquirePersistentMap,
            .ReadbackFromGpu = Ops_ReadbackFromGpu,
        };
    } // namespace

    Bool VkBufferManager::Initialize(const VkBufferManagerInitInfo& initInfo) {
        Shutdown();

        MOBILEGL_ASSERT(initInfo.allocator != nullptr, "VkBufferManager::Initialize requires valid allocator");
        MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkBufferManager::Initialize requires non-zero frame count");

        m_initInfo = initInfo;
        m_deferredBufferReleases.resize(initInfo.frameCount);
        m_deferredResourceReleases.resize(initInfo.frameCount);
        m_currentFrameIndex = 0;
        m_frameSerial = 1;
        m_completedSerialFloor = 0;
        if (!InitializeTransientArenas()) {
            return false;
        }
        g_activeBufferManager = this;
        MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps);
        return true;
    }

    void VkBufferManager::Shutdown() {
        if (g_activeBufferManager == this) {
            g_activeBufferManager = nullptr;
            if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) {
                MG_State::GLState::SetBufferBackendOps(nullptr);
            }
        }
        m_transientUploadArena.Shutdown();
        m_unboundStorageBuffer.Destroy();
        m_unboundTexelBuffer.Destroy();
        DestroyAllDeferredReleases();
        ReleaseAllLiveResources();
        m_copyProvider = nullptr;
        m_initInfo = {};
        m_currentFrameIndex = 0;
        m_frameSerial = 1;
        m_completedSerialFloor = 0;
    }

    Bool VkBufferManager::RecreateTransientArenas(Uint32 frameCount) {
        MOBILEGL_ASSERT(m_initInfo.allocator != nullptr,
                        "VkBufferManager::RecreateTransientArenas requires initialized manager");
        MOBILEGL_ASSERT(frameCount > 0, "VkBufferManager::RecreateTransientArenas requires non-zero frame count");

        // Callers guarantee the device is idle around arena recreation.
        NotifyDeviceIdle();
        m_transientUploadArena.Shutdown();
        m_initInfo.frameCount = frameCount;
        DestroyAllDeferredReleases();
        m_deferredBufferReleases.resize(frameCount);
        m_deferredResourceReleases.resize(frameCount);
        m_currentFrameIndex = 0;
        return InitializeTransientArenas();
    }

    void VkBufferManager::BeginFrame(Uint32 frameIndex) {
        MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(),
                        "VkBufferManager::BeginFrame frame index out of range");
        m_currentFrameIndex = frameIndex;
        ++m_frameSerial;
        CollectDeferredReleases(frameIndex);
        m_transientUploadArena.BeginFrame(frameIndex);
    }

    void VkBufferManager::CollectAllDeferredReleases() {
        // Per-resource releases only. Every one of them was deferred behind a BumpSliceEpoch,
        // so no memo can still name the handle, and the caller has proved the GPU is idle.
        //
        // The transient arena's releases are deliberately NOT collected here. A buffer lands
        // there when the arena outgrows it mid-frame (BufferArena::EnsureCapacity), and at
        // that moment every slice already handed out from this frame's arena still names it -
        // VkBufferResource::transientSlice above all, which AcquireStreamedSlice keeps
        // serving for the whole frame serial on the strength of transientFrameSerial alone.
        // Nothing bumps the slice epoch for those other resources, so freeing the buffer
        // here left the streamed memo handing a destroyed VkBuffer to vkCmdBindIndexBuffer
        // (llvmpipe then faulted inside the draw; the Create/Flywheel indirect retrace died
        // exactly this way). Mid-frame drains do not advance m_frameSerial, so they must not
        // free arena storage either: the arena's own ResetFrame/BeginFrame is the point where
        // the slot's slices stop being reachable, and that is where these releases land.
        for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
            CollectDeferredReleases(frameIndex);
        }
    }

    void VkBufferManager::NotifyDeviceIdle() {
        // Everything submitted so far has completed. Work recorded for the
        // current frame has not been submitted yet, so the current serial
        // remains busy.
        if (m_frameSerial > 0) {
            m_completedSerialFloor = m_frameSerial - 1;
        }
    }

    void VkBufferManager::NotifyFrameSerialComplete(Uint64 serial) {
        // The current serial's work is still being recorded; a completion
        // report for it (or beyond) can only come from a stale caller.
        if (serial >= m_frameSerial) {
            return;
        }
        m_completedSerialFloor = std::max(m_completedSerialFloor, serial);
    }

    void VkBufferManager::SetCopyCommandProvider(IBufferCopyCommandProvider* provider) {
        m_copyProvider = provider;
    }

    Uint64 VkBufferManager::GetCompletedSerial() const {
        const Uint64 frameCount = m_initInfo.frameCount > 0 ? m_initInfo.frameCount : 1;
        const Uint64 completed = m_frameSerial > frameCount ? m_frameSerial - frameCount : 0;
        return std::max(completed, m_completedSerialFloor);
    }

    Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const {
        return resource.lastUseSerial > GetCompletedSerial();
    }

    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);
    }

    Bool VkBufferManager::InitializeTransientArenas() {
        return m_transientUploadArena.Initialize({
            .allocator = m_initInfo.allocator,
            .frameCount = m_initInfo.frameCount,
            .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
                     VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT |
                     VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
            .memoryUsage = m_initInfo.transientMemoryUsage,
            .allocationFlags = m_initInfo.transientAllocationFlags,
            .minBufferSize = m_initInfo.minUploadBytes,
            .persistentlyMapped = m_initInfo.transientPersistentMapping,
        });
    }

    VkBufferResource* VkBufferManager::ResourceOf(MG_State::GLState::BufferObject& bufferObject) {
        return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
    }

    VkBufferResource* VkBufferManager::GetOrCreateResource(
        const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
        // Return by raw pointer: the resource is owned for its whole lifetime by the BufferObject's
        // backend-resource SharedPtr (already set, or set below), so callers that only dereference
        // it avoid a static_pointer_cast + SharedPtr refcount inc/dec on every per-draw buffer bind.
        const auto& existing = bufferObject->GetBackendResource();
        if (existing) {
            return static_cast<VkBufferResource*>(existing.get());
        }
        auto resource = MakeShared<VkBufferResource>();
        VkBufferResource* raw = resource.get();
        bufferObject->SetBackendResource(resource);
        TrackLiveResource(resource);
        return raw;
    }

    void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
        // Sweep on a doubling watermark rather than on every insert past the threshold. The old
        // form walked the whole vector for each new buffer once the list passed 256, and when the
        // buffers are all live the walk removes nothing and the list grows by one - so creating N
        // live buffers cost ~N^2/2 expired() checks. Reclamation semantics are unchanged: the sweep
        // still removes exactly the expired entries, just less often and with the same bound on how
        // much dead weight can accumulate (at most as many entries as were live at the last sweep).
        if (m_liveResources.size() >= std::max<SizeT>(kLiveResourcePruneThreshold, 2 * m_liveResourcesLastPruned)) {
            std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& weak) { return weak.expired(); });
            m_liveResourcesLastPruned = m_liveResources.size();
        }
        m_liveResources.push_back(resource);
    }

    void VkBufferManager::ReleaseAllLiveResources() {
        for (auto& weak : m_liveResources) {
            if (auto resource = weak.lock()) {
                BumpSliceEpoch(*resource);
                resource->buffer.Destroy();
                resource->storageSize = 0;
                resource->usageFlags = 0;
                resource->lastUseSerial = 0;
                resource->pendingFullUpload = true;
                resource->transientSlice = {};
                resource->transientFrameSerial = 0;
            }
        }
        m_liveResources.clear();
    }

    Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size,
                                                VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) {
        // The only place a resident VkBuffer handle is minted, so every resident slice
        // change funnels through here (callers release the old handle first).
        BumpSliceEpoch(resource);
        // Staged range copies write resident storage with vkCmdCopyBuffer.
        usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
        const Bool created = resource.buffer.Create({
            .allocator = m_initInfo.allocator,
            .size = size,
            .usage = usage,
            .memoryUsage = VMA_MEMORY_USAGE_AUTO,
            .allocationFlags = kResidentBufferAllocationFlags,
            .requiredFlags = requiredFlags,
        });
        if (!created || resource.buffer.Map() == nullptr) {
            MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
                    static_cast<unsigned long long>(size));
            resource.buffer.Destroy();
            resource.storageSize = 0;
            resource.usageFlags = 0;
            return false;
        }
        resource.storageSize = size;
        resource.usageFlags = usage;
        return true;
    }

    Bool VkBufferManager::SwapStorageAndUploadAll(VkBufferResource& resource,
                                                  MG_State::GLState::BufferObject& bufferObject) {
        const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
        const VkBufferUsageFlags usage = resource.usageFlags;
        DeferRelease(std::move(resource.buffer));
        if (!CreateResidentStorage(resource, size, usage)) {
            resource.pendingFullUpload = true;
            return false;
        }
        if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
            MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
            resource.pendingFullUpload = true;
            return false;
        }
        resource.pendingFullUpload = false;
        return true;
    }

    Bool VkBufferManager::StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject,
                                          SizeT offset, SizeT size) {
        if (!m_copyProvider) {
            return false;
        }
        BufferSlice staging{};
        if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject.MappedData() + offset,
                                           static_cast<VkDeviceSize>(size), 16, staging)) {
            return false;
        }
        VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
        if (commandBuffer == VK_NULL_HANDLE) {
            return false;
        }

        // Order the copy after every prior read/write of this buffer, both from
        // in-flight frames (submission order) and from commands already recorded
        // in this frame's command buffer.
        VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
        beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
        beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
                             &beforeBarrier, 0, nullptr, 0, nullptr);

        VkBufferCopy region{};
        region.srcOffset = staging.offset;
        region.dstOffset = static_cast<VkDeviceSize>(offset);
        region.size = static_cast<VkDeviceSize>(size);
        vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, &region);

        VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
        afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
                             &afterBarrier, 0, nullptr, 0, nullptr);

        resource.lastUseSerial = m_frameSerial;
        return true;
    }

    void VkBufferManager::OnRespecify(MG_State::GLState::BufferObject& bufferObject) {
        auto* resource = ResourceOf(bufferObject);
        if (!resource) {
            return; // lazy: AcquireResidentSlice performs a full upload on creation
        }
        // A respecify can change the size, the usage hint (so the resident/streamed
        // route), and the contents at once; retire every memo before deciding what to
        // do about the storage.
        BumpSliceEpoch(*resource);
        // Any cached streaming slice refers to the previous contents.
        resource->transientFrameSerial = 0;
        // Redefining the store hands any adopted mapping back to the CPU shadow
        // (BufferObject::RedefineStorage), so a buffer that reaches here persistent-mapped
        // is an ordinary resident one again: it needs the busy-tracking and conditional
        // orphan below, and the next AcquirePersistentMap has to mint storage for the new
        // store rather than hand back a mapping of the old one.
        resource->persistentMapped = false;
        if (!resource->buffer.IsValid()) {
            return; // streaming-only resource: shadow + serial are enough
        }

        const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
        if (size == 0) {
            DeferRelease(std::move(resource->buffer));
            resource->storageSize = 0;
            resource->pendingFullUpload = false;
            return;
        }

        if (size != resource->storageSize || IsResourceBusy(*resource)) {
            // Conditional orphan: only swap the storage when the old one is
            // still referenced by the GPU (or no longer fits).
            SwapStorageAndUploadAll(*resource, bufferObject);
            return;
        }

        if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
            MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
            resource->pendingFullUpload = true;
        }
    }

    void VkBufferManager::OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size) {
        auto* resource = ResourceOf(bufferObject);
        if (!resource) {
            return;
        }
        // Drops the streaming memo below and may end in a storage swap or a deferred
        // full re-upload, so no memoised slice survives this.
        BumpSliceEpoch(*resource);
        resource->transientFrameSerial = 0;
        if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
            return;
        }
        if (static_cast<VkDeviceSize>(bufferObject.GetSize()) != resource->storageSize) {
            resource->pendingFullUpload = true;
            return;
        }

        if (!IsResourceBusy(*resource)) {
            if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
                                         static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
                MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
                resource->pendingFullUpload = true;
            }
            return;
        }

        // Busy partial write: stage + GPU copy preserves GL ordering within the
        // frame and leaves bytes outside the range (possibly GPU-written, e.g.
        // SSBO) intact. Fall back to a storage swap if staging is unavailable.
        if (!StagedRangeCopy(*resource, bufferObject, offset, size)) {
            SwapStorageAndUploadAll(*resource, bufferObject);
        }
    }

    void VkBufferManager::OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
                                             Flags<BufferMappingAccessBit> appAccess) {
        auto* resource = ResourceOf(bufferObject);
        if (!resource) {
            return;
        }
        BumpSliceEpoch(*resource);
        resource->transientFrameSerial = 0;
        if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
            return;
        }
        if (static_cast<VkDeviceSize>(bufferObject.GetSize()) != resource->storageSize) {
            resource->pendingFullUpload = true;
            return;
        }

        const SizeT offset = range.start;
        const SizeT size = range.end - range.start;
        // GL_MAP_UNSYNCHRONIZED_BIT: the app guarantees it does not overwrite
        // data the GPU is still reading; honour it with a direct host write.
        if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
            if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
                                         static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
                MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
                resource->pendingFullUpload = true;
            }
            return;
        }

        if (!StagedRangeCopy(*resource, bufferObject, offset, size)) {
            SwapStorageAndUploadAll(*resource, bufferObject);
        }
    }

    void VkBufferManager::OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource) {
        if (!resource) {
            return;
        }
        auto vkResource = std::static_pointer_cast<VkBufferResource>(std::move(resource));
        if (!vkResource->buffer.IsValid()) {
            return;
        }
        if (m_deferredResourceReleases.empty()) {
            vkResource->buffer.Destroy();
            return;
        }
        MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredResourceReleases.size(),
                        "VkBufferManager::OnResourceDestroyed current frame index out of range");
        // Keep the whole resource alive until this frame slot's fence has been
        // waited, then the storage is destroyed with it.
        m_deferredResourceReleases[m_currentFrameIndex].push_back(std::move(vkResource));
    }

    void* VkBufferManager::AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject) {
        const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
        if (size == 0) {
            return nullptr;
        }

        auto resource = std::static_pointer_cast<VkBufferResource>(bufferObject.GetBackendResource());
        if (!resource) {
            resource = MakeShared<VkBufferResource>();
            bufferObject.SetBackendResource(resource);
            TrackLiveResource(resource);
        }

        // Bumped for the request, not just for the storage it may create. This is the
        // one call the frontend makes when a buffer becomes persistently mapped for
        // writing (BufferObject::AcquireMemoryRange), and a map the backend declines
        // keeps mutating its shadow with no further API call - so it is what lets
        // GetSliceEpochCounter stand for "no buffer needs a persistent-map range push".
        BumpSliceEpoch(*resource);

        // Idempotent: an already-backed buffer returns the same mapped base.
        if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
            return resource->buffer.GetMappedData();
        }

        // One-time creation of HOST_VISIBLE + HOST_COHERENT, persistently mapped storage
        // carrying every usage (never recreated, so the app's pointer never dangles). Seed
        // it from the current shadow - MappedData() is still the shadow here because the
        // frontend adopts (and drops) the shadow only after this returns.
        DeferRelease(std::move(resource->buffer));
        const VkBufferUsageFlags persistentUsage =
            kPersistentBackedUsage |
            (m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
        if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
            resource->persistentMapped = false;
            resource->storageSize = 0;
            resource->usageFlags = 0;
            return nullptr;
        }
        const Uint8* seed = bufferObject.MappedData();
        if (seed != nullptr) {
            resource->buffer.Upload(seed, size, 0);
        }
        resource->persistentMapped = true;
        resource->pendingFullUpload = false;
        resource->storageSize = size;
        resource->lastUseSerial = 0;
        return resource->buffer.GetMappedData();
    }

    Bool VkBufferManager::AcquireResidentSlice(BufferKind kind,
                                               const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
                                               BufferSlice& outSlice) {
        const VkBufferUsageFlags requiredUsage = GetVkBufferUsage(kind);
        MOBILEGL_ASSERT(requiredUsage != 0, "VkBufferManager::AcquireResidentSlice unsupported buffer kind");
        MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireResidentSlice requires valid buffer object");

        auto resource = GetOrCreateResource(bufferObject);
        bufferObject->SyncPersistentMappedRange();

        const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
        if (size == 0) {
            MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
            return false;
        }

        // Zero-copy persistent buffers already hold the app's live coherent writes in
        // host-visible storage carrying every usage; bind directly, no re-upload/staging.
        if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
            resource->lastUseSerial = m_frameSerial;
            outSlice = resource->buffer.GetSlice(0, size);
            return outSlice.IsValid();
        }

        const Bool needsRecreate = !resource->buffer.IsValid() || resource->storageSize != size ||
                                   ((resource->usageFlags & requiredUsage) != requiredUsage) ||
                                   resource->pendingFullUpload;
        if (needsRecreate) {
            const VkBufferUsageFlags usage = resource->usageFlags | requiredUsage;
            DeferRelease(std::move(resource->buffer));
            if (!CreateResidentStorage(*resource, size, usage)) {
                return false;
            }
            if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
                MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
                resource->buffer.Destroy();
                resource->storageSize = 0;
                resource->usageFlags = 0;
                return false;
            }
            resource->pendingFullUpload = false;
        }

        resource->lastUseSerial = m_frameSerial;
        outSlice = resource->buffer.GetSlice(0, size);
        return true;
    }

    Bool VkBufferManager::AcquireStreamedSlice(BufferKind kind,
                                               const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
                                               BufferSlice& outSlice) {
        (void)kind;
        MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireStreamedSlice requires valid buffer object");

        auto resource = GetOrCreateResource(bufferObject);
        bufferObject->SyncPersistentMappedRange();

        // A persistently mapped resource's storage IS the application's copy of the bytes -
        // the frontend adopted it in place of the shadow and hands out pointers into it, and
        // a shader can have written bytes the shadow never saw (a transform feedback
        // capture). Streaming a second copy would feed this draw the stale shadow, and the
        // downgrade below would release the storage the application still points at,
        // breaking the "never recreated" promise AcquirePersistentMap makes.
        if (resource->persistentMapped) {
            return AcquireResidentSlice(kind, bufferObject, outSlice);
        }

        const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
        if (size == 0) {
            MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
            return false;
        }

        const Uint64 changeSerial = bufferObject->GetChangeSerial();
        if (resource->transientFrameSerial == m_frameSerial && resource->transientChangeSerial == changeSerial &&
            resource->transientSize == size && resource->transientSlice.IsValid()) {
            outSlice = resource->transientSlice;
            return true;
        }

        // Idle-content promotion: see the field comments in VkBufferResource. The
        // streak counts frame BOUNDARIES survived unchanged (the same-frame memo
        // above swallows repeat draws), so a promotion needs the content stable
        // for kStreamedPromotionStreak whole frames - one no-op frame does not
        // trigger the resident round-trip, whose creation upload is itself a
        // staged copy worth avoiding for content that is about to change again.
        constexpr Uint32 kStreamedPromotionStreak = 2;
        if (resource->promotedResident) {
            if (resource->promotedChangeSerial == changeSerial &&
                static_cast<VkDeviceSize>(bufferObject->GetSize()) == size) {
                return AcquireResidentSlice(kind, bufferObject, outSlice);
            }
            resource->promotedResident = false;
            resource->unchangedStreak = 0;
        } else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
                   resource->transientFrameSerial != 0) {
            if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
                // Promotion moves the buffer off the arena and onto resident storage.
                resource->promotedResident = true;
                resource->promotedChangeSerial = changeSerial;
                BumpSliceEpoch(*resource);
                if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
                    return true;
                }
                resource->promotedResident = false; // resident creation failed: stream as before
            }
        } else {
            resource->unchangedStreak = 0;
        }

        // A fresh arena allocation: a different slice than the last call handed back,
        // and (below) the point where a promoted buffer's resident storage is released.
        // The stable-promotion exit above returns before this, so a buffer the app has
        // stopped touching keeps one slice for as long as it keeps its resident storage.
        BumpSliceEpoch(*resource);
        if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
                                           outSlice)) {
            return false;
        }
        resource->transientSlice = outSlice;
        resource->transientFrameSerial = m_frameSerial;
        resource->transientChangeSerial = changeSerial;
        resource->transientSize = size;

        // Streaming path is authoritative now; release resident storage so we do
        // not keep a second, stale copy alive (downgrade).
        if (resource->buffer.IsValid()) {
            DeferRelease(std::move(resource->buffer));
            resource->storageSize = 0;
        }
        return true;
    }

    void VkBufferManager::DeferRelease(VkBufferObject&& buffer) {
        if (!buffer.IsValid()) {
            return;
        }

        if (m_deferredBufferReleases.empty()) {
            buffer.Destroy();
            return;
        }

        MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredBufferReleases.size(),
                        "VkBufferManager::DeferRelease current frame index out of range");
        m_deferredBufferReleases[m_currentFrameIndex].push_back(std::move(buffer));
    }

    void VkBufferManager::CollectDeferredReleases(Uint32 frameIndex) {
        MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(),
                        "VkBufferManager::CollectDeferredReleases frame index out of range");
        m_deferredBufferReleases[frameIndex].clear();
        m_deferredResourceReleases[frameIndex].clear();
    }

    BufferSlice VkBufferManager::AcquireUnboundStorageDescriptor() {
        if (!m_unboundStorageBuffer.IsValid()) {
            if (m_initInfo.allocator == nullptr) {
                return {};
            }
            // Host-visible so the zero fill needs no command buffer: this can be reached from
            // descriptor resolution, which runs inside an already-open recording and must not
            // start a copy of its own. The size is a whole minStorageBufferOffsetAlignment-safe
            // block rather than 4 bytes so that a shader which does read the block gets a
            // plausible unsized-array length instead of one that rounds to zero.
            const Bool created = m_unboundStorageBuffer.Create({
                .allocator = m_initInfo.allocator,
                .size = kUnboundStorageDescriptorBytes,
                .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                .memoryUsage = VMA_MEMORY_USAGE_AUTO,
                .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
                                   VMA_ALLOCATION_CREATE_MAPPED_BIT,
                .requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
            });
            if (!created) {
                MGLOG_E_ONCE("VkBufferManager::AcquireUnboundStorageDescriptor: placeholder creation failed");
                m_unboundStorageBuffer.Destroy();
                return {};
            }
            if (void* mapped = m_unboundStorageBuffer.GetMappedData()) {
                Memset(mapped, 0, static_cast<SizeT>(kUnboundStorageDescriptorBytes));
            }
        }
        return m_unboundStorageBuffer.GetSlice();
    }

    BufferSlice VkBufferManager::AcquireUnboundTexelBufferDescriptor() {
        if (!m_unboundTexelBuffer.IsValid()) {
            if (m_initInfo.allocator == nullptr) {
                return {};
            }
            // A SECOND placeholder rather than more usage bits on the storage-block one. The two
            // are independent failure domains: a device that refuses this allocation must not
            // take the storage-block placeholder - and with it the fix this one is a sibling of -
            // down with it. Host-visible and zero-filled for the same reason as that one: this is
            // reached from descriptor resolution, inside an already-open recording, which must
            // not start a copy of its own.
            const Bool created = m_unboundTexelBuffer.Create({
                .allocator = m_initInfo.allocator,
                .size = kUnboundTexelBufferDescriptorBytes,
                .usage = VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT |
                         VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                .memoryUsage = VMA_MEMORY_USAGE_AUTO,
                .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
                                   VMA_ALLOCATION_CREATE_MAPPED_BIT,
                .requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
            });
            if (!created) {
                MGLOG_E_ONCE("VkBufferManager::AcquireUnboundTexelBufferDescriptor: placeholder creation failed");
                m_unboundTexelBuffer.Destroy();
                return {};
            }
            if (void* mapped = m_unboundTexelBuffer.GetMappedData()) {
                Memset(mapped, 0, static_cast<SizeT>(kUnboundTexelBufferDescriptorBytes));
            }
        }
        return m_unboundTexelBuffer.GetSlice();
    }

    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:
            return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
        case BufferKind::TextureBuffer:
            // Both texel roles, for the same reason vertex/index carry both bits: one GL buffer
            // texture can be read as a samplerBuffer and written as an imageBuffer, and which of
            // the two it is only becomes known when a shader that uses it is bound - long after
            // the resident buffer was created. A VkBufferView for a storage-texel descriptor is
            // invalid unless the buffer was created with the storage bit, so a buffer that
            // acquired only the uniform bit could never be given one.
            return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
        case BufferKind::ShaderStorage:
            return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
        case BufferKind::Indirect:
            return VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
        default:
            return 0;
        }
    }

    void VkBufferManager::DestroyAllDeferredReleases() {
        for (auto& releases : m_deferredBufferReleases) {
            for (auto& buffer : releases) {
                buffer.Destroy();
            }
            releases.clear();
        }
        m_deferredBufferReleases.clear();
        for (auto& releases : m_deferredResourceReleases) {
            for (auto& resource : releases) {
                resource->buffer.Destroy();
            }
            releases.clear();
        }
        m_deferredResourceReleases.clear();
    }
} // namespace MobileGL::MG_Backend::DirectVulkan
