blob: d5c0dc1b12462b21bf75edb9c6de5398b4363e57 [file] [log] [blame]
//
// 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,
RenderPassClosureReason 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,
RenderPassClosureReason 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_