//
// Copyright 2017 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Resource:
//    Resource lifetime tracking in the Vulkan back-end.
//

#ifndef LIBANGLE_RENDERER_VULKAN_RESOURCEVK_H_
#define LIBANGLE_RENDERER_VULKAN_RESOURCEVK_H_

#include "common/FixedQueue.h"
#include "common/SimpleMutex.h"
#include "libANGLE/HandleAllocator.h"
#include "libANGLE/renderer/vulkan/vk_utils.h"

#include <queue>

namespace rx
{
namespace vk
{
// We expect almost all reasonable usage case should have at most 4 current contexts now. When
// exceeded, it should still work, but storage will grow.
static constexpr size_t kMaxFastQueueSerials = 4;
// Serials is an array of queue serials, which when paired with the index of the serials in the
// array result in QueueSerials. The array may expand if needed. Since it owned by Resource object
// which is protected by shared lock, it is safe to reallocate storage if needed. When it passes to
// renderer at garbage collection time, we will make a copy. The array size is expected to be small.
// But in future if we run into situation that array size is too big, we can change to packed array
// of QueueSerials.
using Serials = angle::FastVector<Serial, kMaxFastQueueSerials>;

// Tracks how a resource is used by ANGLE and by a VkQueue. The serial indicates the most recent use
// of a resource in the VkQueue. We use the monotonically incrementing serial number to determine if
// a resource is currently in use.
class ResourceUse final
{
  public:
    ResourceUse()  = default;
    ~ResourceUse() = default;

    ResourceUse(const QueueSerial &queueSerial) { setQueueSerial(queueSerial); }
    ResourceUse(const Serials &otherSerials) { mSerials = otherSerials; }

    // Copy constructor
    ResourceUse(const ResourceUse &other) : mSerials(other.mSerials) {}
    ResourceUse &operator=(const ResourceUse &other)
    {
        mSerials = other.mSerials;
        return *this;
    }

    // Move constructor
    ResourceUse(ResourceUse &&other) : mSerials(other.mSerials) { other.mSerials.clear(); }
    ResourceUse &operator=(ResourceUse &&other)
    {
        mSerials = other.mSerials;
        other.mSerials.clear();
        return *this;
    }

    bool valid() const { return mSerials.size() > 0; }

    void reset() { mSerials.clear(); }

    const Serials &getSerials() const { return mSerials; }

    void setSerial(SerialIndex index, Serial serial)
    {
        ASSERT(index != kInvalidQueueSerialIndex);
        if (ANGLE_UNLIKELY(mSerials.size() <= index))
        {
            mSerials.resize(index + 1, kZeroSerial);
        }
        ASSERT(mSerials[index] <= serial);
        mSerials[index] = serial;
    }

    void setQueueSerial(const QueueSerial &queueSerial)
    {
        setSerial(queueSerial.getIndex(), queueSerial.getSerial());
    }

    // Returns true if there is at least one serial is greater than
    bool operator>(const AtomicQueueSerialFixedArray &serials) const
    {
        ASSERT(mSerials.size() <= serials.size());
        for (SerialIndex i = 0; i < mSerials.size(); ++i)
        {
            if (mSerials[i] > serials[i])
            {
                return true;
            }
        }
        return false;
    }

    // Compare the recorded serial with the parameter
    bool operator>(const QueueSerial &queuSerial) const
    {
        return mSerials.size() > queuSerial.getIndex() &&
               mSerials[queuSerial.getIndex()] > queuSerial.getSerial();
    }
    bool operator>=(const QueueSerial &queueSerial) const
    {
        return mSerials.size() > queueSerial.getIndex() &&
               mSerials[queueSerial.getIndex()] >= queueSerial.getSerial();
    }

    // Returns true if all serials are less than or equal
    bool operator<=(const AtomicQueueSerialFixedArray &serials) const
    {
        ASSERT(mSerials.size() <= serials.size());
        for (SerialIndex i = 0; i < mSerials.size(); ++i)
        {
            if (mSerials[i] > serials[i])
            {
                return false;
            }
        }
        return true;
    }

    bool usedByCommandBuffer(const QueueSerial &commandBufferQueueSerial) const
    {
        ASSERT(commandBufferQueueSerial.valid());
        // Return true if we have the exact queue serial in the array.
        return mSerials.size() > commandBufferQueueSerial.getIndex() &&
               mSerials[commandBufferQueueSerial.getIndex()] ==
                   commandBufferQueueSerial.getSerial();
    }

    // Merge other's serials into this object.
    void merge(const ResourceUse &other)
    {
        if (mSerials.size() < other.mSerials.size())
        {
            mSerials.resize(other.mSerials.size(), kZeroSerial);
        }

        for (SerialIndex i = 0; i < other.mSerials.size(); ++i)
        {
            if (mSerials[i] < other.mSerials[i])
            {
                mSerials[i] = other.mSerials[i];
            }
        }
    }

  private:
    // The most recent time of use in a VkQueue.
    Serials mSerials;
};
std::ostream &operator<<(std::ostream &os, const ResourceUse &use);

class SharedGarbage final : angle::NonCopyable
{
  public:
    SharedGarbage();
    SharedGarbage(SharedGarbage &&other);
    SharedGarbage(const ResourceUse &use, GarbageObjects &&garbage);
    ~SharedGarbage();
    SharedGarbage &operator=(SharedGarbage &&rhs);

    bool destroyIfComplete(Renderer *renderer);
    bool hasResourceUseSubmitted(Renderer *renderer) const;
    // This is not being used now.
    VkDeviceSize getSize() const { return 0; }

  private:
    ResourceUse mLifetime;
    GarbageObjects mGarbage;
};

// SharedGarbageList list tracks garbage using angle::FixedQueue. It allows concurrent add (i.e.,
// enqueue) and cleanup (i.e. dequeue) operations from two threads. Add call from two threads are
// synchronized using a mutex and cleanup call from two threads are synchronized with a separate
// mutex.
template <class T>
class SharedGarbageList final : angle::NonCopyable
{
  public:
    SharedGarbageList()
        : mSubmittedQueue(kInitialQueueCapacity),
          mUnsubmittedQueue(kInitialQueueCapacity),
          mTotalSubmittedGarbageBytes(0),
          mTotalUnsubmittedGarbageBytes(0),
          mTotalGarbageDestroyed(0)
    {}
    ~SharedGarbageList()
    {
        ASSERT(mSubmittedQueue.empty());
        ASSERT(mUnsubmittedQueue.empty());
    }

    void add(Renderer *renderer, T &&garbage)
    {
        VkDeviceSize size = garbage.getSize();
        if (garbage.destroyIfComplete(renderer))
        {
            mTotalGarbageDestroyed += size;
        }
        else
        {
            std::unique_lock<angle::SimpleMutex> enqueueLock(mMutex);
            if (garbage.hasResourceUseSubmitted(renderer))
            {
                addGarbageLocked(mSubmittedQueue, std::move(garbage));
                mTotalSubmittedGarbageBytes += size;
            }
            else
            {
                addGarbageLocked(mUnsubmittedQueue, std::move(garbage));
                // We use relaxed ordering here since it is always modified with mMutex. The atomic
                // is only for the purpose of make tsan happy.
                mTotalUnsubmittedGarbageBytes.fetch_add(size, std::memory_order_relaxed);
            }
        }
    }

    bool empty() const { return mSubmittedQueue.empty() && mUnsubmittedQueue.empty(); }
    VkDeviceSize getSubmittedGarbageSize() const
    {
        return mTotalSubmittedGarbageBytes.load(std::memory_order_consume);
    }
    VkDeviceSize getUnsubmittedGarbageSize() const
    {
        return mTotalUnsubmittedGarbageBytes.load(std::memory_order_consume);
    }
    VkDeviceSize getDestroyedGarbageSize() const
    {
        return mTotalGarbageDestroyed.load(std::memory_order_consume);
    }
    void resetDestroyedGarbageSize() { mTotalGarbageDestroyed = 0; }

    // Number of bytes destroyed is returned.
    VkDeviceSize cleanupSubmittedGarbage(Renderer *renderer)
    {
        std::unique_lock<angle::SimpleMutex> lock(mSubmittedQueueDequeueMutex);
        VkDeviceSize bytesDestroyed = 0;
        while (!mSubmittedQueue.empty())
        {
            T &garbage        = mSubmittedQueue.front();
            VkDeviceSize size = garbage.getSize();
            if (!garbage.destroyIfComplete(renderer))
            {
                break;
            }
            bytesDestroyed += size;
            mSubmittedQueue.pop();
        }
        mTotalSubmittedGarbageBytes -= bytesDestroyed;
        mTotalGarbageDestroyed += bytesDestroyed;
        return bytesDestroyed;
    }

    // Check if pending garbage is still pending submission. If not, move them to the garbage list.
    // Otherwise move the element to the end of the queue. Note that this call took both locks of
    // this list. Since this call is only used for pending submission garbage list and that list
    // only temporary stores garbage, it does not destroy garbage in this list. And moving garbage
    // around is expected to be cheap in general, so lock contention is not expected.
    void cleanupUnsubmittedGarbage(Renderer *renderer)
    {
        std::unique_lock<angle::SimpleMutex> enqueueLock(mMutex);
        size_t count            = mUnsubmittedQueue.size();
        VkDeviceSize bytesMoved = 0;
        for (size_t i = 0; i < count; i++)
        {
            T &garbage = mUnsubmittedQueue.front();
            if (garbage.hasResourceUseSubmitted(renderer))
            {
                bytesMoved += garbage.getSize();
                addGarbageLocked(mSubmittedQueue, std::move(garbage));
            }
            else
            {
                mUnsubmittedQueue.push(std::move(garbage));
            }
            mUnsubmittedQueue.pop();
        }
        mTotalUnsubmittedGarbageBytes -= bytesMoved;
        mTotalSubmittedGarbageBytes += bytesMoved;
    }

  private:
    void addGarbageLocked(angle::FixedQueue<T> &queue, T &&garbage)
    {
        // Expand the queue storage if we only have one empty space left. That one empty space is
        // required by cleanupPendingSubmissionGarbage so that we do not need to allocate another
        // temporary storage.
        if (queue.size() >= queue.capacity() - 1)
        {
            std::unique_lock<angle::SimpleMutex> dequeueLock(mSubmittedQueueDequeueMutex);
            size_t newCapacity = queue.capacity() << 1;
            queue.updateCapacity(newCapacity);
        }
        queue.push(std::move(garbage));
    }

    static constexpr size_t kInitialQueueCapacity = 64;
    // Protects both enqueue and dequeue of mUnsubmittedQueue, as well as enqueue of
    // mSubmittedQueue.
    angle::SimpleMutex mMutex;
    // Protect dequeue of mSubmittedQueue, which is expected to be more expensive.
    angle::SimpleMutex mSubmittedQueueDequeueMutex;
    // Holds garbage that all of use has been submitted to renderer.
    angle::FixedQueue<T> mSubmittedQueue;
    // Holds garbage with at least one of the queueSerials has not yet submitted to renderer.
    angle::FixedQueue<T> mUnsubmittedQueue;
    // Total bytes of garbage in mSubmittedQueue.
    std::atomic<VkDeviceSize> mTotalSubmittedGarbageBytes;
    // Total bytes of garbage in mUnsubmittedQueue.
    std::atomic<VkDeviceSize> mTotalUnsubmittedGarbageBytes;
    // Total bytes of garbage been destroyed since last resetDestroyedGarbageSize call.
    std::atomic<VkDeviceSize> mTotalGarbageDestroyed;
};

// This is a helper class for back-end objects used in Vk command buffers. They keep a record
// of their use in ANGLE and VkQueues via ResourceUse.
class Resource : angle::NonCopyable
{
  public:
    virtual ~Resource() {}

    // Complete all recorded and in-flight commands involving this resource
    angle::Result waitForIdle(ContextVk *contextVk,
                              const char *debugMessage,
                              QueueSubmitReason reason);

    void setSerial(SerialIndex index, Serial serial) { mUse.setSerial(index, serial); }

    void setQueueSerial(const QueueSerial &queueSerial)
    {
        mUse.setSerial(queueSerial.getIndex(), queueSerial.getSerial());
    }

    void mergeResourceUse(const ResourceUse &use) { mUse.merge(use); }

    // Check if this resource is used by a command buffer.
    bool usedByCommandBuffer(const QueueSerial &commandBufferQueueSerial) const
    {
        return mUse.usedByCommandBuffer(commandBufferQueueSerial);
    }

    const ResourceUse &getResourceUse() const { return mUse; }

  protected:
    Resource() {}
    Resource(Resource &&other) : Resource() { mUse = std::move(other.mUse); }
    Resource &operator=(Resource &&rhs)
    {
        std::swap(mUse, rhs.mUse);
        return *this;
    }

    // Current resource lifetime.
    ResourceUse mUse;
};

// Similar to |Resource| above, this tracks object usage. This includes additional granularity to
// track whether an object is used for read-only or read/write access.
class ReadWriteResource : public Resource
{
  public:
    virtual ~ReadWriteResource() override {}

    // Complete all recorded and in-flight commands involving this resource
    angle::Result waitForIdle(ContextVk *contextVk,
                              const char *debugMessage,
                              QueueSubmitReason reason)
    {
        return Resource::waitForIdle(contextVk, debugMessage, reason);
    }

    void setWriteQueueSerial(const QueueSerial &writeQueueSerial)
    {
        mUse.setQueueSerial(writeQueueSerial);
        mWriteUse.setQueueSerial(writeQueueSerial);
    }

    // Check if this resource is used by a command buffer.
    bool usedByCommandBuffer(const QueueSerial &commandBufferQueueSerial) const
    {
        return mUse.usedByCommandBuffer(commandBufferQueueSerial);
    }
    bool writtenByCommandBuffer(const QueueSerial &commandBufferQueueSerial) const
    {
        return mWriteUse.usedByCommandBuffer(commandBufferQueueSerial);
    }

    const ResourceUse &getWriteResourceUse() const { return mWriteUse; }

  protected:
    ReadWriteResource() {}
    ReadWriteResource(ReadWriteResource &&other) { *this = std::move(other); }
    ReadWriteResource &operator=(ReadWriteResource &&other)
    {
        Resource::operator=(std::move(other));
        mWriteUse = std::move(other.mWriteUse);
        return *this;
    }

    // Track write use of the object. Only updated for setWriteQueueSerial().
    ResourceUse mWriteUse;
};

// Adds "void release(Renderer *)" method for collecting garbage.
// Enables RendererScoped<> for classes that support DeviceScoped<>.
template <class T>
class ReleasableResource final : public Resource
{
  public:
    // Calls collectGarbage() on the object.
    void release(Renderer *renderer);

    const T &get() const { return mObject; }
    T &get() { return mObject; }

  private:
    T mObject;
};
}  // namespace vk
}  // namespace rx

#endif  // LIBANGLE_RENDERER_VULKAN_RESOURCEVK_H_
