blob: 5a4c3d8fe21e3ef19af0b48e5776856f61c85a04 [file] [log] [blame] [edit]
//
// Copyright 2023 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.
//
// SharedContextMutex.h: Classes for protecting Shared Context access and EGLImage siblings.
#ifndef LIBANGLE_SHARED_CONTEXT_MUTEX_H_
#define LIBANGLE_SHARED_CONTEXT_MUTEX_H_
#include <atomic>
#include "common/debug.h"
namespace gl
{
class Context;
}
namespace egl
{
#if defined(ANGLE_ENABLE_SHARED_CONTEXT_MUTEX)
constexpr bool kIsSharedContextMutexEnabled = true;
#else
constexpr bool kIsSharedContextMutexEnabled = false;
#endif
class ContextMutex : angle::NonCopyable
{
public:
// Below group of methods are not thread safe and requires external synchronization.
// Note: since release*() may call onDestroy(), additional synchronization requirements may be
// enforced by concrete implementations.
ANGLE_INLINE void addRef() { ++mRefCount; }
ANGLE_INLINE void release() { release(UnlockBehaviour::kNotUnlock); }
ANGLE_INLINE void releaseAndUnlock() { release(UnlockBehaviour::kUnlock); }
ANGLE_INLINE bool isReferenced() const { return mRefCount > 0; }
virtual bool try_lock() = 0;
virtual void lock() = 0;
virtual void unlock() = 0;
protected:
virtual ~ContextMutex();
enum class UnlockBehaviour
{
kNotUnlock,
kUnlock
};
void release(UnlockBehaviour unlockBehaviour);
virtual void onDestroy(UnlockBehaviour unlockBehaviour);
protected:
size_t mRefCount = 0;
};
struct ContextMutexMayBeNullTag final
{};
constexpr ContextMutexMayBeNullTag kContextMutexMayBeNull;
// Prevents destruction while locked, uses mMutex to protect addRef()/releaseAndUnlock() calls.
class [[nodiscard]] ScopedContextMutexAddRefLock final : angle::NonCopyable
{
public:
ANGLE_INLINE ScopedContextMutexAddRefLock() = default;
ANGLE_INLINE explicit ScopedContextMutexAddRefLock(ContextMutex *mutex) { lock(mutex); }
ANGLE_INLINE ScopedContextMutexAddRefLock(ContextMutex *mutex, ContextMutexMayBeNullTag)
{
if (mutex != nullptr)
{
lock(mutex);
}
}
ANGLE_INLINE ~ScopedContextMutexAddRefLock()
{
if (mMutex != nullptr)
{
mMutex->releaseAndUnlock();
}
}
private:
void lock(ContextMutex *mutex);
private:
ContextMutex *mMutex = nullptr;
};
class [[nodiscard]] ScopedContextMutexLock final
{
public:
ANGLE_INLINE ScopedContextMutexLock() = default;
ANGLE_INLINE ScopedContextMutexLock(ContextMutex *mutex, gl::Context *context)
: mMutex(mutex), mContext(context)
{
ASSERT(mutex != nullptr);
ASSERT(context != nullptr);
mutex->lock();
}
ANGLE_INLINE ScopedContextMutexLock(ContextMutex *mutex,
gl::Context *context,
ContextMutexMayBeNullTag)
: mMutex(mutex), mContext(context)
{
if (ANGLE_LIKELY(mutex != nullptr))
{
ASSERT(context != nullptr);
mutex->lock();
}
}
ANGLE_INLINE ~ScopedContextMutexLock()
{
if (ANGLE_LIKELY(mMutex != nullptr))
{
ASSERT(IsContextMutexStateConsistent(mContext));
mMutex->unlock();
}
}
ANGLE_INLINE ScopedContextMutexLock(ScopedContextMutexLock &&other)
: mMutex(other.mMutex), mContext(other.mContext)
{
other.mMutex = nullptr;
}
ANGLE_INLINE ScopedContextMutexLock &operator=(ScopedContextMutexLock &&other)
{
std::swap(mMutex, other.mMutex);
mContext = other.mContext;
return *this;
}
private:
static bool IsContextMutexStateConsistent(gl::Context *context);
private:
ContextMutex *mMutex = nullptr;
gl::Context *mContext = nullptr;
};
// Mutex may be locked only by a single thread. Other threads may only check the status.
class SingleContextMutex final : public ContextMutex
{
public:
ANGLE_INLINE bool isLocked(std::memory_order order) const { return mState.load(order) > 0; }
// ContextMutex
bool try_lock() override;
void lock() override;
void unlock() override;
private:
std::atomic_int mState{0};
};
// Note: onDestroy() method must be protected by "this" mutex, since onDestroy() is called from
// release*() methods, these methods must also be protected by "this" mutex.
template <class Mutex>
class SharedContextMutex final : public ContextMutex
{
public:
SharedContextMutex();
~SharedContextMutex() override;
// Merges mutexes so they work as one.
// At the end, only single "root" mutex will be locked.
// Does nothing if two mutexes are the same or already merged (have same "root" mutex).
// Note: synchronization requirements for addRef()/release*() calls for merged mutexes are
// the same as for the single unmerged mutex. For example: can't call at the same time
// mutexA.addRef() and mutexB.release() if they are merged.
static void Merge(SharedContextMutex *lockedMutex, SharedContextMutex *otherMutex);
// Returns current "root" mutex.
// Warning! Result is only stable if mutex is locked, while may change any time if unlocked.
// May be used to compare against already locked "root" mutex.
ANGLE_INLINE SharedContextMutex *getRoot() { return mRoot.load(std::memory_order_relaxed); }
// ContextMutex
bool try_lock() override;
void lock() override;
void unlock() override;
private:
SharedContextMutex *doTryLock();
SharedContextMutex *doLock();
void doUnlock();
// All methods below must be protected by "this" mutex ("stable root" in "this" instance).
void setNewRoot(SharedContextMutex *newRoot);
void addLeaf(SharedContextMutex *leaf);
void removeLeaf(SharedContextMutex *leaf);
// ContextMutex
void onDestroy(UnlockBehaviour unlockBehaviour) override;
private:
Mutex mMutex;
// Used when ASSERT() and/or recursion are/is enabled.
std::atomic<angle::ThreadId> mOwnerThreadId;
// Used only when recursion is enabled.
uint32_t mLockLevel;
// mRoot and mLeaves tree structure details:
// - used to implement primary functionality of this class;
// - initially, all mutexes are "root"s;
// - "root" mutex has "mRoot == this";
// - "root" mutex stores unreferenced pointers to all its leaves (used in merging);
// - "leaf" mutex holds reference (addRef) to the current "root" mutex in the mRoot;
// - "leaf" mutex has empty mLeaves;
// - "leaf" mutex can't become a "root" mutex;
// - before locking the mMutex, "this" is an "unstable root" or a "leaf";
// - the implementation always locks mRoot's mMutex ("unstable root");
// - if after locking the mMutex "mRoot != this", then "this" is/become a "leaf";
// - otherwise, "this" is a locked "stable root" - lock is successful.
std::atomic<SharedContextMutex *> mRoot;
std::set<SharedContextMutex *> mLeaves;
// mOldRoots is used to solve a particular problem (below example does not use mRank):
// - have "leaf" mutex_2 with a reference to mutex_1 "root";
// - the mutex_1 has no other references (only in the mutex_2);
// - have other mutex_3 "root";
// - mutex_1 pointer is cached on the stack during locking of mutex_2 (thread A);
// - merge mutex_3 and mutex_2 (thread B):
// * now "leaf" mutex_2 stores reference to mutex_3 "root";
// * old "root" mutex_1 becomes a "leaf" of mutex_3;
// * old "root" mutex_1 has no references and gets destroyed.
// - invalid pointer to destroyed mutex_1 stored on the stack and in the mLeaves of mutex_3;
// - to fix this problem, references to old "root"s are kept in the mOldRoots vector.
std::vector<SharedContextMutex *> mOldRoots;
// mRank is used to fix a problem of indefinite grows of mOldRoots:
// - merge mutex_2 and mutex_1 -> mutex_2 is "root" of mutex_1 (mOldRoots == 0);
// - destroy mutex_2;
// - merge mutex_3 and mutex_1 -> mutex_3 is "root" of mutex_1 (mOldRoots == 1);
// - destroy mutex_3;
// - merge mutex_4 and mutex_1 -> mutex_4 is "root" of mutex_1 (mOldRoots == 2);
// - destroy mutex_4;
// - continuing this pattern can lead to indefinite grows of mOldRoots, while pick number of
// mutexes is only 2.
// Fix details using mRank:
// - initially "mRank == 0" and only relevant for "root" mutexes;
// - merging mutexes with equal mRank of their "root"s, will use first (lockedMutex) "root"
// mutex as a new "root" and increase its mRank by 1;
// - otherwise, "root" mutex with a highest rank will be used without changing the mRank;
// - this way, "stronger" (with a higher mRank) "root" mutex will "protect" its "leaves" from
// "mRoot" replacement and therefore - mOldRoots grows.
// Lets look at the problematic pattern with the mRank:
// - merge mutex_2 and mutex_1 -> mutex_2 is "root" (mRank == 1) of mutex_1 (mOldRoots == 0);
// - destroy mutex_2;
// - merge mutex_3 and mutex_1 -> mutex_2 is "root" (mRank == 1) of mutex_3 (mOldRoots == 0);
// - destroy mutex_3;
// - merge mutex_4 and mutex_1 -> mutex_2 is "root" (mRank == 1) of mutex_4 (mOldRoots == 0);
// - destroy mutex_4;
// - no mOldRoots grows at all;
// - minumum number of mutexes to reach mOldRoots size of N => 2^(N+1).
uint32_t mRank;
};
class ContextMutexManager
{
public:
virtual ~ContextMutexManager() = default;
virtual ContextMutex *create() = 0;
virtual void merge(ContextMutex *lockedMutex, ContextMutex *otherMutex) = 0;
virtual ContextMutex *getRootMutex(ContextMutex *mutex) = 0;
};
template <class Mutex>
class SharedContextMutexManager final : public ContextMutexManager
{
public:
ContextMutex *create() override;
void merge(ContextMutex *lockedMutex, ContextMutex *otherMutex) override;
ContextMutex *getRootMutex(ContextMutex *mutex) override;
};
} // namespace egl
#endif // LIBANGLE_SHARED_CONTEXT_MUTEX_H_