blob: 80d35980147697c95066b036ca423a9844a20b3c [file] [log] [blame]
//
// 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.
//
// ContextMutex.h: Classes for protecting Context access and EGLImage siblings.
#ifndef LIBANGLE_CONTEXT_MUTEX_H_
#define LIBANGLE_CONTEXT_MUTEX_H_
#include <atomic>
#include "common/debug.h"
namespace gl
{
class Context;
}
namespace egl
{
#if defined(ANGLE_ENABLE_CONTEXT_MUTEX)
constexpr bool kIsContextMutexEnabled = true;
#else
constexpr bool kIsContextMutexEnabled = false;
#endif
// Use standard mutex for now
using ContextMutexType = std::mutex;
class ContextMutex final : angle::NonCopyable
{
public:
explicit ContextMutex(ContextMutex *root = nullptr);
// For "leaf" mutex its "root" must be locked during destructor call.
~ContextMutex();
// 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).
static void Merge(ContextMutex *lockedMutex, ContextMutex *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 ContextMutex *getRoot() { return mRoot.load(std::memory_order_relaxed); }
ANGLE_INLINE const ContextMutex *getRoot() const
{
return mRoot.load(std::memory_order_relaxed);
}
// Below group of methods are not thread safe and must be protected by "this" mutex instance.
ANGLE_INLINE void addRef() { ++mRefCount; }
ANGLE_INLINE void release() { release(UnlockBehaviour::kDoNotUnlock); }
// Must be only called on "root" mutex.
ANGLE_INLINE void releaseAndUnlock() { release(UnlockBehaviour::kUnlock); }
ANGLE_INLINE bool isReferenced() const { return mRefCount > 0; }
bool try_lock();
void lock();
void unlock();
private:
enum class UnlockBehaviour
{
kDoNotUnlock,
kUnlock
};
bool tryLockImpl();
void lockImpl();
void unlockImpl();
// All methods below must be protected by "this" mutex ("stable root" in "this" instance).
void setNewRoot(ContextMutex *newRoot);
void addLeaf(ContextMutex *leaf);
void removeLeaf(ContextMutex *leaf);
void release(UnlockBehaviour unlockBehaviour);
private:
// 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.
// 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.
// 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).
std::atomic<ContextMutex *> mRoot;
ContextMutexType mMutex;
// Used when ASSERT() and/or recursion are/is enabled.
std::atomic<angle::ThreadId> mOwnerThreadId;
// Used only when recursion is enabled.
uint32_t mLockLevel;
size_t mRefCount;
std::set<ContextMutex *> mLeaves;
std::vector<ContextMutex *> mOldRoots;
uint32_t mRank;
};
// 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)
{
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 explicit ScopedContextMutexLock(ContextMutex &mutex) : mMutex(&mutex)
{
mutex.lock();
}
ANGLE_INLINE ScopedContextMutexLock(ContextMutex *mutex) : mMutex(mutex)
{
if (ANGLE_LIKELY(mutex != nullptr))
{
mutex->lock();
}
}
ANGLE_INLINE ~ScopedContextMutexLock()
{
if (ANGLE_LIKELY(mMutex != nullptr))
{
mMutex->unlock();
}
}
ANGLE_INLINE ScopedContextMutexLock(ScopedContextMutexLock &&other) : mMutex(other.mMutex)
{
other.mMutex = nullptr;
}
ANGLE_INLINE ScopedContextMutexLock &operator=(ScopedContextMutexLock &&other)
{
std::swap(mMutex, other.mMutex);
return *this;
}
private:
ContextMutex *mMutex = nullptr;
};
} // namespace egl
#endif // LIBANGLE_CONTEXT_MUTEX_H_