From e7cf5df1911682d30fb096f8d2179722a3268b8d Mon Sep 17 00:00:00 2001 From: Jacob Wirth Date: Thu, 30 Mar 2023 03:29:13 -0400 Subject: [PATCH 1/8] Optimize lock release when lock is being passed around as subset --- inc/Tecs.hh | 4 +- inc/Tecs_entity.hh | 123 ++++++++++--- inc/Tecs_lock.hh | 246 +++++++++++++++++++++----- inc/Tecs_permissions.hh | 2 +- inc/Tecs_storage.hh | 55 +++++- inc/Tecs_tracing.hh | 2 + inc/Tecs_transaction.hh | 370 +++++++++++++++++++++++----------------- tests/tests.cpp | 184 ++++++++++++++++++-- tests/tests.hh | 3 + 9 files changed, 744 insertions(+), 245 deletions(-) diff --git a/inc/Tecs.hh b/inc/Tecs.hh index 1a027c7..75eba71 100644 --- a/inc/Tecs.hh +++ b/inc/Tecs.hh @@ -188,10 +188,8 @@ namespace Tecs { template friend class Lock; - template + template friend class Transaction; - template typename, typename...> - friend class BaseTransaction; friend struct Entity; }; } // namespace Tecs diff --git a/inc/Tecs_entity.hh b/inc/Tecs_entity.hh index e257ab4..55f26ea 100644 --- a/inc/Tecs_entity.hh +++ b/inc/Tecs_entity.hh @@ -24,7 +24,10 @@ static_assert(sizeof(TECS_ENTITY_GENERATION_TYPE) > sizeof(TECS_ENTITY_ECS_IDENT namespace Tecs { struct Entity; -}; + + template + class EntityLock; +}; // namespace Tecs namespace std { inline string to_string(const Tecs::Entity &ent); @@ -51,6 +54,8 @@ namespace Tecs { return (TECS_ENTITY_ECS_IDENTIFIER_TYPE)(generation >> generationBits); } + using namespace std::string_literals; + struct Entity { // Workaround for Clang so that std::atomic operations can be inlined as if uint64. See issue: // https://stackoverflow.com/questions/60445848/clang-doesnt-inline-stdatomicload-for-loading-64-bit-structs @@ -68,14 +73,23 @@ namespace Tecs { public: template inline bool Exists(LockType &lock) const { - auto &metadataList = - lock.permissions[0] ? lock.instance.metadata.writeComponents : lock.instance.metadata.readComponents; + auto &metadataList = lock.writePermissions[0] ? lock.instance.metadata.writeComponents + : lock.instance.metadata.readComponents; if (index >= metadataList.size()) return false; auto &metadata = metadataList[index]; return metadata[0] && metadata.generation == generation; } + template + inline bool Exists(EntityLock &lock) const { + if (*this == lock.entity) { + return Exists(lock.lock); + } else { + return Existed(lock.lock); + } + } + template inline bool Existed(LockType &lock) const { if (index >= lock.instance.metadata.readComponents.size()) return false; @@ -84,11 +98,16 @@ namespace Tecs { return metadata[0] && metadata.generation == generation; } + template + inline bool Existed(EntityLock &lock) const { + return Existed(lock.lock); + } + template inline bool Has(LockType &lock) const { static_assert(!contains_global_components(), "Entities cannot have global components"); - auto &metadataList = - lock.permissions[0] ? lock.instance.metadata.writeComponents : lock.instance.metadata.readComponents; + auto &metadataList = lock.writePermissions[0] ? lock.instance.metadata.writeComponents + : lock.instance.metadata.readComponents; if (index >= metadataList.size()) return false; auto &metadata = metadataList[index]; @@ -96,6 +115,16 @@ namespace Tecs { lock.instance.template BitsetHas(metadata); } + template + inline bool Has(EntityLock &lock) const { + static_assert(!contains_global_components(), "Entities cannot have global components"); + if (*this == lock.entity) { + return Has(lock.lock); + } else { + return Had(lock.lock); + } + } + template inline bool Had(LockType &lock) const { static_assert(!contains_global_components(), "Entities cannot have global components"); @@ -106,6 +135,12 @@ namespace Tecs { lock.instance.template BitsetHas(metadata); } + template + inline bool Had(EntityLock &lock) const { + static_assert(!contains_global_components(), "Entities cannot have global components"); + return Had(lock.lock); + } + template, LockType>::value, T, const T>> @@ -118,8 +153,8 @@ namespace Tecs { if constexpr (!std::is_const()) lock.base->template SetAccessFlag(true); - auto &metadataList = - lock.permissions[0] ? lock.instance.metadata.writeComponents : lock.instance.metadata.readComponents; + auto &metadataList = lock.writePermissions[0] ? lock.instance.metadata.writeComponents + : lock.instance.metadata.readComponents; if (index >= metadataList.size()) { throw std::runtime_error("Entity does not exist: " + std::to_string(*this)); } @@ -140,18 +175,36 @@ namespace Tecs { lock.instance.template Storage().validEntityIndexes[index] = validEntities.size(); validEntities.emplace_back(*this); } else { - throw std::runtime_error( - "Entity does not have a component of type: " + std::string(typeid(CompType).name())); + throw std::runtime_error("Entity does not have a component of type: "s + typeid(CompType).name()); } } - if (lock.instance.template BitsetHas(lock.permissions)) { + if (lock.instance.template BitsetHas(lock.writePermissions)) { return lock.instance.template Storage().writeComponents[index]; } else { return lock.instance.template Storage().readComponents[index]; } } + template, Permissions...>::value, T, const T>> + inline ReturnType &Get(EntityLock &lock) const { + using CompType = std::remove_cv_t; + static_assert(is_read_allowed(), "Component is not locked for reading."); + static_assert(is_write_allowed() || std::is_const(), + "Can't get non-const reference of read only Component."); + static_assert(!is_global_component(), "Global components must be accessed through lock.Get()"); + if (*this == lock.entity) { + return Get(lock.lock); + } else if constexpr (std::is_const_v) { + return GetPrevious(lock.lock); + } else { + throw std::runtime_error("Entity is not locked for writing: " + std::to_string(*this) + + " lock is for " + std::to_string(lock.entity)); + } + } + template inline const T &GetPrevious(LockType &lock) const { using CompType = std::remove_cv_t; @@ -169,20 +222,28 @@ namespace Tecs { } if (!lock.instance.template BitsetHas(metadata)) { - throw std::runtime_error( - "Entity does not have a component of type: " + std::string(typeid(CompType).name())); + throw std::runtime_error("Entity does not have a component of type: "s + typeid(CompType).name()); } return lock.instance.template Storage().readComponents[index]; } + template + inline const T &GetPrevious(EntityLock &lock) const { + using CompType = std::remove_cv_t; + static_assert(is_read_allowed(), "Component is not locked for reading."); + static_assert(!is_global_component(), + "Global components must be accessed through lock.GetPrevious()"); + return GetPrevious(lock.lock); + } + template inline T &Set(LockType &lock, T &value) const { static_assert(is_write_allowed(), "Component is not locked for writing."); static_assert(!is_global_component(), "Global components must be accessed through lock.Set()"); lock.base->template SetAccessFlag(true); - auto &metadataList = - lock.permissions[0] ? lock.instance.metadata.writeComponents : lock.instance.metadata.readComponents; + auto &metadataList = lock.writePermissions[0] ? lock.instance.metadata.writeComponents + : lock.instance.metadata.readComponents; if (index >= metadataList.size()) { throw std::runtime_error("Entity does not exist: " + std::to_string(*this)); } @@ -201,21 +262,32 @@ namespace Tecs { lock.instance.template Storage().validEntityIndexes[index] = validEntities.size(); validEntities.emplace_back(*this); } else { - throw std::runtime_error( - "Entity does not have a component of type: " + std::string(typeid(T).name())); + throw std::runtime_error("Entity does not have a component of type: "s + typeid(T).name()); } } return lock.instance.template Storage().writeComponents[index] = value; } + template + inline T &Set(EntityLock &lock, T &value) const { + static_assert(is_write_allowed(), "Component is not locked for writing."); + static_assert(!is_global_component(), "Global components must be accessed through lock.Set()"); + if (*this == lock.entity) { + return Set(lock.lock, value); + } else { + throw std::runtime_error("Entity is not locked for writing: " + std::to_string(*this) + + " lock is for " + std::to_string(lock.entity)); + } + } + template inline T &Set(LockType &lock, Args... args) const { static_assert(is_write_allowed(), "Component is not locked for writing."); static_assert(!is_global_component(), "Global components must be accessed through lock.Set()"); lock.base->template SetAccessFlag(true); - auto &metadataList = - lock.permissions[0] ? lock.instance.metadata.writeComponents : lock.instance.metadata.readComponents; + auto &metadataList = lock.writePermissions[0] ? lock.instance.metadata.writeComponents + : lock.instance.metadata.readComponents; if (index >= metadataList.size()) { throw std::runtime_error("Entity does not exist: " + std::to_string(*this)); } @@ -234,13 +306,24 @@ namespace Tecs { lock.instance.template Storage().validEntityIndexes[index] = validEntities.size(); validEntities.emplace_back(*this); } else { - throw std::runtime_error( - "Entity does not have a component of type: " + std::string(typeid(T).name())); + throw std::runtime_error("Entity does not have a component of type: "s + typeid(T).name()); } } return lock.instance.template Storage().writeComponents[index] = T(std::forward(args)...); } + template + inline T &Set(EntityLock &lock, Args... args) const { + static_assert(is_write_allowed(), "Component is not locked for writing."); + static_assert(!is_global_component(), "Global components must be accessed through lock.Set()"); + if (*this == lock.entity) { + return Set(lock.lock, std::forward(args)...); + } else { + throw std::runtime_error("Entity is not locked for writing: " + std::to_string(*this) + + " lock is for " + std::to_string(lock.entity)); + } + } + template inline void Unset(LockType &lock) const { static_assert(is_add_remove_allowed(), "Components cannot be removed without an AddRemove lock."); diff --git a/inc/Tecs_lock.hh b/inc/Tecs_lock.hh index 3708bb0..3ade311 100644 --- a/inc/Tecs_lock.hh +++ b/inc/Tecs_lock.hh @@ -24,6 +24,8 @@ static_assert(TECS_ENTITY_ALLOCATION_BATCH_SIZE > 0, "At least 1 entity needs to namespace Tecs { template class DynamicLock; + template + class EntityLock; /** * Lock is a reference to lock permissions held by an active Transaction. @@ -51,30 +53,24 @@ namespace Tecs { using LockType = Lock; ECS &instance; - std::shared_ptr> base; - std::bitset<1 + sizeof...(AllComponentTypes)> permissions; - - // Private constructor for DynamicLock to Lock conversion - template - inline Lock(ECS &instance, decltype(base) base, decltype(permissions) permissions) - : instance(instance), base(base), permissions(permissions) {} + std::shared_ptr> base; + std::bitset<1 + sizeof...(AllComponentTypes)> writePermissions; public: // Start a new transaction inline Lock(ECS &instance) : instance(instance) { - base = std::make_shared>(instance); - permissions[0] = is_add_remove_allowed(); - // clang-format off - (( - permissions[1 + instance.template GetComponentIndex()] = is_write_allowed() - ), ...); - // clang-format on + base = std::make_shared>(*this); + base->readLockReferences[0]++; + if constexpr (is_add_remove_allowed()) base->writeLockReferences[0]++; + (base->template AcquireLockReference(), ...); + + writePermissions = base->writePermissions; } // Returns true if this lock type can be constructed from a lock with the specified source permissions - template + template static constexpr bool is_lock_subset() { - using SourceLockType = Lock; + using SourceLockType = Lock; if constexpr (is_add_remove_allowed() && !is_add_remove_allowed()) { return false; } else { @@ -89,9 +85,27 @@ namespace Tecs { } // Reference an existing transaction - template(), int> = 0> - inline Lock(const Lock &source) - : instance(source.instance), base(source.base), permissions(source.permissions) {} + inline Lock(const LockType &source) + : instance(source.instance), base(source.base), writePermissions(source.writePermissions) { + base->readLockReferences[0]++; + if constexpr (is_add_remove_allowed()) base->writeLockReferences[0]++; + (base->template AcquireLockReference(), ...); + } + + // Reference a subset of an existing transaction + template(), int> = 0> + inline Lock(const Lock &source) + : instance(source.instance), base(source.base), writePermissions(source.writePermissions) { + base->readLockReferences[0]++; + if constexpr (is_add_remove_allowed()) base->writeLockReferences[0]++; + (base->template AcquireLockReference(), ...); + } + + inline ~Lock() { + base->readLockReferences[0]--; + if constexpr (is_add_remove_allowed()) base->writeLockReferences[0]--; + (base->template ReleaseLockReference(), ...); + } inline constexpr ECS &GetInstance() const { return instance; @@ -114,7 +128,7 @@ namespace Tecs { inline const EntityView EntitiesWith() const { static_assert(!is_global_component(), "Entities can't have global components"); - if (permissions[0]) { + if (writePermissions[0]) { return instance.template Storage().writeValidEntities; } else { return instance.template Storage().readValidEntities; @@ -126,7 +140,7 @@ namespace Tecs { } inline const EntityView Entities() const { - if (permissions[0]) { + if (writePermissions[0]) { return instance.metadata.writeValidEntities; } else { return instance.metadata.readValidEntities; @@ -179,7 +193,7 @@ namespace Tecs { inline bool Has() const { static_assert(all_global_components(), "Only global components can be accessed without an Entity"); - if (permissions[0]) { + if (writePermissions[0]) { return instance.template BitsetHas(instance.globalWriteMetadata); } else { return instance.template BitsetHas(instance.globalReadMetadata); @@ -204,7 +218,7 @@ namespace Tecs { if (!std::is_const()) base->template SetAccessFlag(true); - auto &metadata = permissions[0] ? instance.globalWriteMetadata : instance.globalReadMetadata; + auto &metadata = writePermissions[0] ? instance.globalWriteMetadata : instance.globalReadMetadata; if (!instance.template BitsetHas(metadata)) { if (is_add_remove_allowed()) { base->writeAccessedFlags[0] = true; @@ -214,11 +228,10 @@ namespace Tecs { // Reset value before allowing reading. instance.template Storage().writeComponents[0] = {}; } else { - throw std::runtime_error( - "Missing global component of type: " + std::string(typeid(CompType).name())); + throw std::runtime_error("Missing global component of type: "s + typeid(CompType).name()); } } - if (instance.template BitsetHas(permissions)) { + if (instance.template BitsetHas(writePermissions)) { return instance.template Storage().writeComponents[0]; } else { return instance.template Storage().readComponents[0]; @@ -232,7 +245,7 @@ namespace Tecs { static_assert(is_global_component(), "Only global components can be accessed without an Entity"); if (!instance.template BitsetHas(instance.globalReadMetadata)) { - throw std::runtime_error("Missing global component of type: " + std::string(typeid(CompType).name())); + throw std::runtime_error("Missing global component of type: "s + typeid(CompType).name()); } return instance.template Storage().readComponents[0]; } @@ -243,7 +256,7 @@ namespace Tecs { static_assert(is_global_component(), "Only global components can be accessed without an Entity"); base->template SetAccessFlag(true); - auto &metadata = permissions[0] ? instance.globalWriteMetadata : instance.globalReadMetadata; + auto &metadata = writePermissions[0] ? instance.globalWriteMetadata : instance.globalReadMetadata; if (!instance.template BitsetHas(metadata)) { if (is_add_remove_allowed()) { base->writeAccessedFlags[0] = true; @@ -251,7 +264,7 @@ namespace Tecs { metadata[1 + instance.template GetComponentIndex()] = true; instance.template Storage().writeComponents.resize(1); } else { - throw std::runtime_error("Missing global component of type: " + std::string(typeid(T).name())); + throw std::runtime_error("Missing global component of type: "s + typeid(T).name()); } } return instance.template Storage().writeComponents[0] = value; @@ -263,7 +276,7 @@ namespace Tecs { static_assert(is_global_component(), "Only global components can be accessed without an Entity"); base->template SetAccessFlag(true); - auto &metadata = permissions[0] ? instance.globalWriteMetadata : instance.globalReadMetadata; + auto &metadata = writePermissions[0] ? instance.globalWriteMetadata : instance.globalReadMetadata; if (!instance.template BitsetHas(metadata)) { if (is_add_remove_allowed()) { base->writeAccessedFlags[0] = true; @@ -271,7 +284,7 @@ namespace Tecs { metadata[1 + instance.template GetComponentIndex()] = true; instance.template Storage().writeComponents.resize(1); } else { - throw std::runtime_error("Missing global component of type: " + std::string(typeid(T).name())); + throw std::runtime_error("Missing global component of type: "s + typeid(T).name()); } } return instance.template Storage().writeComponents[0] = T(std::forward(args)...); @@ -305,8 +318,8 @@ namespace Tecs { template inline Lock Subset() const { - using NewLockType = Lock; - static_assert(has_permissions(), "Lock types are not a subset of existing permissions."); + static_assert(has_permissions(), + "Lock types are not a subset of existing permissions."); return Lock(*this); } @@ -316,6 +329,15 @@ namespace Tecs { } private: + // Private constructor for DynamicLock to Lock conversion + template + inline Lock(ECS &instance, decltype(base) base, decltype(writePermissions) writePermissions) + : instance(instance), base(base), writePermissions(writePermissions) { + base->readLockReferences[0]++; + if constexpr (is_add_remove_allowed()) base->writeLockReferences[0]++; + (base->template AcquireLockReference(), ...); + } + template inline void AllocateComponents(size_t count) const { if constexpr (!is_global_component()) { @@ -364,10 +386,14 @@ namespace Tecs { (RemoveComponents(index), ...); } + template + friend class Transaction; template friend class Lock; template friend class DynamicLock; + template + friend class EntityLock; friend struct Entity; }; @@ -377,8 +403,6 @@ namespace Tecs { private: using ECS = ECSType; - const std::bitset<1 + sizeof...(AllComponentTypes)> readPermissions; - template static const auto generateReadBitset() { std::bitset<1 + sizeof...(AllComponentTypes)> result; @@ -401,22 +425,160 @@ namespace Tecs { public: template - DynamicLock(const LockType &lock) - : Lock(lock), readPermissions(generateReadBitset()) {} + inline DynamicLock(const LockType &source) : Lock(source) {} template std::optional> TryLock() const { if constexpr (Lock::template has_permissions()) { - return Lock(this->instance, this->base, this->permissions); + return Lock(this->instance, this->base, this->writePermissions); } else { static const auto requestedRead = generateReadBitset(); static const auto requestedWrite = generateWriteBitset(); - if ((requestedRead & readPermissions) == requestedRead && - (requestedWrite & this->permissions) == requestedWrite) { - return Lock(this->instance, this->base, this->permissions); + if ((requestedRead & this->base->readPermissions) == requestedRead && + (requestedWrite & this->base->writePermissions) == requestedWrite) { + return Lock(this->instance, this->base, this->writePermissions); } return {}; } } + + template + friend class DynamicLock; + }; + + template typename ECSType, typename... AllComponentTypes, typename... Permissions> + class EntityLock, Permissions...> { + private: + using ECS = ECSType; + using LockType = Lock; + + const LockType lock; + + public: + const Entity entity; + + template + inline EntityLock(const SourceLockType &source, const Entity &entity) : lock(source), entity(entity) { + static_assert(!is_add_remove_allowed(), + "EntityLock with AddRemove permissions is not supported"); + } + + template + inline EntityLock(const EntityLock &source) + : lock(source.lock), entity(source.entity) {} + + inline constexpr ECS &GetInstance() const { + return lock.instance; + } + +#ifndef TECS_HEADER_ONLY + inline size_t GetTransactionId() const { + return lock.base->transactionId; + } +#endif + + template + inline const EntityView PreviousEntitiesWith() const { + static_assert(!is_global_component(), "Entities can't have global components"); + return lock.template PreviousEntitiesWith(); + } + + template + inline const EntityView EntitiesWith() const { + static_assert(!is_global_component(), "Entities can't have global components"); + return lock.template EntitiesWith(); + } + + inline const EntityView PreviousEntities() const { + return lock.template PreviousEntities(); + } + + inline const EntityView Entities() const { + return lock.template Entities(); + } + + inline bool Exists() const { + return entity.Exists(lock); + } + + inline bool Existed() const { + return entity.Existed(lock); + } + + template + inline bool Has() const { + if constexpr (all_global_components()) { + return lock.template Had(); + } else { + return entity.Has(lock); + } + } + + template + inline bool Had() const { + if constexpr (all_global_components()) { + return lock.template Had(); + } else { + return entity.Had(lock); + } + } + + template + inline auto &Get() const { + using CompType = std::remove_cv_t; + static_assert(is_read_allowed(), "Component is not locked for reading."); + if constexpr (is_global_component()) { + return lock.template Get(); + } else { + return entity.Get(lock); + } + } + + template + inline const T &GetPrevious() const { + using CompType = std::remove_cv_t; + static_assert(is_read_allowed(), "Component is not locked for reading."); + if constexpr (is_global_component()) { + return lock.template GetPrevious(); + } else { + return entity.GetPrevious(lock); + } + } + + template + inline T &Set(T &value) const { + static_assert(is_write_allowed(), "Component is not locked for writing."); + if constexpr (is_global_component()) { + return lock.template Set(value); + } else { + return entity.Set(value, lock); + } + } + + template + inline T &Set(Args &&...args) const { + static_assert(is_write_allowed(), "Component is not locked for writing."); + if constexpr (is_global_component()) { + return lock.template Set(std::forward(args)...); + } else { + return entity.Set(std::forward(args)..., lock); + } + } + + template + inline EntityLock Subset() const { + static_assert(LockType::template has_permissions(), + "Lock types are not a subset of existing permissions."); + + return EntityLock(*this); + } + + long UseCount() const { + return lock.base.use_count(); + } + + template + friend class EntityLock; + friend struct Entity; }; -}; // namespace Tecs +} // namespace Tecs diff --git a/inc/Tecs_permissions.hh b/inc/Tecs_permissions.hh index 3b242e9..d71ec53 100644 --- a/inc/Tecs_permissions.hh +++ b/inc/Tecs_permissions.hh @@ -12,7 +12,7 @@ namespace Tecs { class Lock {}; template class DynamicLock {}; - template + template class Transaction {}; /** diff --git a/inc/Tecs_storage.hh b/inc/Tecs_storage.hh index 1a6dcda..14a8507 100644 --- a/inc/Tecs_storage.hh +++ b/inc/Tecs_storage.hh @@ -282,17 +282,18 @@ namespace Tecs { traceInfo.Trace(TraceEvent::Type::WriteUnlock); #endif - // Unlock read and write copies + // Release exclusive read lock if a commit lock is held uint32_t current = readers; if (current == READER_LOCKED) { if (!readers.compare_exchange_strong(current, READER_FREE, std::memory_order_release)) { throw std::runtime_error("WriteUnlock readers changed unexpectedly"); } - } #if __cpp_lib_atomic_wait - readers.notify_all(); + readers.notify_all(); #endif + } + // Relase write lock current = writer; if (current != WRITER_LOCKED && current != WRITER_COMMIT) { throw std::runtime_error("WriteUnlock called outside of WriteLock"); @@ -303,6 +304,48 @@ namespace Tecs { writer.notify_all(); #endif +#ifdef TECS_ENABLE_TRACY + tracyWrite.AfterUnlock(); +#endif + } + + // Transitions between a write lock acquired state to a read lock acquired state. + // Read locks cannot transitioned to write locks without starting a new transaction. + inline void WriteToReadLock() { +#ifdef TECS_ENABLE_PERFORMANCE_TRACING + traceInfo.Trace(TraceEvent::Type::WriteToReadLock); +#endif + + // Acquire a shared read lock, releasing the exclusive read lock if a commit lock is held + uint32_t current = readers; + if (current == READER_LOCKED) { + if (!readers.compare_exchange_strong(current, READER_FREE + 1, std::memory_order_release)) { + throw std::runtime_error("WriteToReadLock readers changed unexpectedly"); + } + } else { + current = readers.fetch_add(1, std::memory_order_acquire); + if (current == READER_LOCKED) { + throw std::runtime_error("WriteToReadLock read lock acquire conflict"); + } + } +#if __cpp_lib_atomic_wait + readers.notify_all(); +#endif +#ifdef TECS_ENABLE_TRACY + tracyRead.AfterTryLockShared(true); +#endif + + // Release write lock + current = writer; + if (current != WRITER_LOCKED && current != WRITER_COMMIT) { + throw std::runtime_error("WriteToReadLock called outside of WriteLock"); + } else if (!writer.compare_exchange_strong(current, WRITER_FREE, std::memory_order_release)) { + throw std::runtime_error("WriteToReadLock writer changed unexpectedly"); + } +#if __cpp_lib_atomic_wait + writer.notify_all(); +#endif + #ifdef TECS_ENABLE_TRACY tracyWrite.AfterUnlock(); #endif @@ -313,12 +356,12 @@ namespace Tecs { #endif #ifdef TECS_ENABLE_TRACY static inline const auto tracyReadCtx = []() -> const tracy::SourceLocationData * { - static const std::string lockName = std::string("Read ") + typeid(T *).name(); + static const std::string lockName = "Read "s + typeid(T *).name(); static const tracy::SourceLocationData srcloc{nullptr, lockName.c_str(), __FILE__, __LINE__, 0}; return &srcloc; }; static inline const auto tracyWriteCtx = []() -> const tracy::SourceLocationData * { - static const std::string lockName = std::string("Write ") + typeid(T *).name(); + static const std::string lockName = "Write "s + typeid(T *).name(); static const tracy::SourceLocationData srcloc{nullptr, lockName.c_str(), __FILE__, __LINE__, 0}; return &srcloc; }; @@ -349,7 +392,7 @@ namespace Tecs { template friend class Lock; - template + template friend class Transaction; friend struct Entity; }; diff --git a/inc/Tecs_tracing.hh b/inc/Tecs_tracing.hh index b9cd2f5..dc1b52d 100644 --- a/inc/Tecs_tracing.hh +++ b/inc/Tecs_tracing.hh @@ -44,6 +44,7 @@ namespace Tecs { CommitLock, CommitUnlock, WriteUnlock, + WriteToReadLock, }; Type type = Type::Invalid; @@ -65,6 +66,7 @@ namespace Tecs { "CommitLock", "CommitUnlock", "WriteUnlock", + "WriteToReadLock", }; return out << eventTypeNames[(size_t)t]; } diff --git a/inc/Tecs_transaction.hh b/inc/Tecs_transaction.hh index 0c9f34c..438b4f6 100644 --- a/inc/Tecs_transaction.hh +++ b/inc/Tecs_transaction.hh @@ -38,9 +38,40 @@ namespace Tecs { * Once a Transaction is deconstructed, all Locks referencing its permissions become invalid. */ template typename ECSType, typename... AllComponentTypes> - class BaseTransaction { + class Transaction> { + private: + using ECS = ECSType; + using EntityMetadata = typename ECS::EntityMetadata; + // using FlatPermissions = typename FlattenPermissions::type; + +#ifdef TECS_ENABLE_TRACY + // static inline const auto tracyCtx = []() -> const tracy::SourceLocationData * { + // static const tracy::SourceLocationData srcloc{"TecsTransaction", + // FlatPermissions::Name(), + // __FILE__, + // __LINE__, + // 0}; + // return &srcloc; + // }; + // #if defined(TRACY_HAS_CALLSTACK) && defined(TRACY_CALLSTACK) + // tracy::ScopedZone tracyZone{tracyCtx(), TRACY_CALLSTACK, true}; + // #else + // tracy::ScopedZone tracyZone{tracyCtx(), true}; + // #endif +#endif + + ECS &instance; +#ifndef TECS_HEADER_ONLY + size_t transactionId; +#endif + + std::bitset<1 + sizeof...(AllComponentTypes)> readPermissions, writePermissions, writeAccessedFlags; + std::array readLockReferences, writeLockReferences; + public: - BaseTransaction(ECSType &instance) : instance(instance) { + template + inline Transaction(LockType &lock) + : instance(lock.instance), readLockReferences({0}), writeLockReferences({0}) { #ifndef TECS_HEADER_ONLY transactionId = ++nextTransactionId; for (size_t i = 0; i < activeTransactionsCount; i++) { @@ -53,99 +84,58 @@ namespace Tecs { } activeTransactions[activeTransactionsCount++] = instance.ecsId; #endif - } - // Delete copy constructor - BaseTransaction(const BaseTransaction &) = delete; - - virtual ~BaseTransaction() { -#ifndef TECS_HEADER_ONLY - auto start = activeTransactions.begin(); - activeTransactionsCount = std::remove(start, start + activeTransactionsCount, instance.ecsId) - start; -#endif - } - - protected: - ECSType &instance; -#ifndef TECS_HEADER_ONLY - size_t transactionId; -#endif - - std::bitset<1 + sizeof...(AllComponentTypes)> writeAccessedFlags; - - template - inline void SetAccessFlag(bool value) { - writeAccessedFlags[1 + instance.template GetComponentIndex()] = value; - } - - template - friend class Lock; - friend struct Entity; - }; - - template - class Transaction, Permissions...> : public BaseTransaction { - private: - using LockType = Lock, Permissions...>; - using EntityMetadata = typename ECS::EntityMetadata; - using FlatPermissions = typename FlattenPermissions::type; - -#ifdef TECS_ENABLE_TRACY - static inline const auto tracyCtx = []() -> const tracy::SourceLocationData * { - static const tracy::SourceLocationData srcloc{"TecsTransaction", - FlatPermissions::Name(), - __FILE__, - __LINE__, - 0}; - return &srcloc; - }; - #if defined(TRACY_HAS_CALLSTACK) && defined(TRACY_CALLSTACK) - tracy::ScopedZone tracyZone{tracyCtx(), TRACY_CALLSTACK, true}; - #else - tracy::ScopedZone tracyZone{tracyCtx(), true}; - #endif -#endif - - public: - inline Transaction(ECS &instance) : BaseTransaction(instance) { #ifdef TECS_ENABLE_PERFORMANCE_TRACING - TECS_EXTERNAL_TRACE_TRANSACTION_STARTING(FlatPermissions::Name()); + // TECS_EXTERNAL_TRACE_TRANSACTION_STARTING(FlatPermissions::Name()); instance.transactionTrace.Trace(TraceEvent::Type::TransactionStart); #endif #ifdef TECS_ENABLE_TRACY ZoneNamedN(tracyScope, "StartTransaction", true); #endif + readPermissions[0] = true; + writePermissions[0] = is_add_remove_allowed(); + // clang-format off + (( + readPermissions[1 + instance.template GetComponentIndex()] = is_read_allowed() + ), ...); + (( + writePermissions[1 + instance.template GetComponentIndex()] = is_write_allowed() + ), ...); + // clang-format on + std::bitset<1 + sizeof...(AllComponentTypes)> acquired; // Templated lambda functions for Lock/Unlock so they can be looped over at runtime. std::array, acquired.size()> lockFuncs = { - [&instance](bool block) { - if (is_add_remove_allowed()) { + [&](bool block) { + if constexpr (is_add_remove_allowed()) { return instance.metadata.WriteLock(block); } else { return instance.metadata.ReadLock(block); } }, - [&instance](bool block) { - if (is_write_allowed()) { + [&](bool block) { + if constexpr (is_write_allowed()) { return instance.template Storage().WriteLock(block); - } else if (is_read_allowed()) { + } else if constexpr (is_read_allowed()) { return instance.template Storage().ReadLock(block); + } else { + (void)block; // Unreferenced parameter warning on MSVC + // This component type isn't part of the lock, skip. + return true; } - // This component type isn't part of the lock, skip. - return true; }...}; std::array, acquired.size()> unlockFuncs = { - [&instance]() { - if (is_add_remove_allowed()) { + [&]() { + if constexpr (is_add_remove_allowed()) { return instance.metadata.WriteUnlock(); } else { return instance.metadata.ReadUnlock(); } }, - [&instance]() { - if (is_write_allowed()) { + [&]() { + if constexpr (is_write_allowed()) { instance.template Storage().WriteUnlock(); - } else if (is_read_allowed()) { + } else if constexpr (is_read_allowed()) { instance.template Storage().ReadUnlock(); } // This component type isn't part of the lock, skip. @@ -173,42 +163,98 @@ namespace Tecs { } } - if (is_add_remove_allowed()) { + if constexpr (is_add_remove_allowed()) { // Init observer event queues std::apply( [](auto &...args) { (args.Init(), ...); }, - this->instance.eventLists); + instance.eventLists); } #ifdef TECS_ENABLE_PERFORMANCE_TRACING - TECS_EXTERNAL_TRACE_TRANSACTION_STARTED(FlatPermissions::Name()); + // TECS_EXTERNAL_TRACE_TRANSACTION_STARTED(FlatPermissions::Name()); #endif } + // Delete copy constructor + Transaction(const Transaction &) = delete; + + template + inline void SetAccessFlag(bool value) { + writeAccessedFlags[1 + instance.template GetComponentIndex()] = value; + } + + template + inline void AcquireLockReference() { + if constexpr (is_write_allowed()) { + readLockReferences[1 + instance.template GetComponentIndex()]++; + writeLockReferences[1 + instance.template GetComponentIndex()]++; + } else if constexpr (is_read_allowed()) { + readLockReferences[1 + instance.template GetComponentIndex()]++; + } + } + + template + inline void ReleaseLockReference() { + if constexpr (is_write_allowed()) { + if (!writePermissions[1 + instance.template GetComponentIndex()]) { + throw std::runtime_error("Write lock not acquired"); + } + if (!readPermissions[1 + instance.template GetComponentIndex()]) { + throw std::runtime_error("Read lock not acquired"); + } + auto writers = --writeLockReferences[1 + instance.template GetComponentIndex()]; + auto readers = --readLockReferences[1 + instance.template GetComponentIndex()]; + if (writeAccessedFlags[0] || writeAccessedFlags[1 + instance.template GetComponentIndex()]) { + return; + } + if (writers == 0) { + writePermissions[1 + instance.template GetComponentIndex()] = false; + if (readers == 0) { + readPermissions[1 + instance.template GetComponentIndex()] = false; + instance.template Storage().WriteUnlock(); + } else { + instance.template Storage().WriteToReadLock(); + } + } + } else if constexpr (is_read_allowed()) { + if (!readPermissions[1 + instance.template GetComponentIndex()]) { + throw std::runtime_error("Read lock not acquired"); + } + auto writers = writeLockReferences[1 + instance.template GetComponentIndex()]; + auto readers = --readLockReferences[1 + instance.template GetComponentIndex()]; + if (writeAccessedFlags[0] || writeAccessedFlags[1 + instance.template GetComponentIndex()]) { + return; + } + if (writers == 0 && readers == 0) { + readPermissions[1 + instance.template GetComponentIndex()] = false; + instance.template Storage().ReadUnlock(); + } + } + } + inline ~Transaction() { #ifdef TECS_ENABLE_PERFORMANCE_TRACING - TECS_EXTERNAL_TRACE_TRANSACTION_ENDING(FlatPermissions::Name()); + // TECS_EXTERNAL_TRACE_TRANSACTION_ENDING(FlatPermissions::Name()); #endif #ifdef TECS_ENABLE_TRACY ZoneNamedN(tracyTxScope, "EndTransaction", true); #endif - if constexpr (is_add_remove_allowed()) { - if (this->writeAccessedFlags[0]) { - PreCommitAddRemoveMetadata(); - (PreCommitAddRemove(), ...); - } + // If an AddRemove was performed, update the entity validity metadata + if (writeAccessedFlags[0]) { + PreCommitAddRemoveMetadata(); + (PreCommitAddRemove(), ...); } ( // For each AllComponentTypes, unlock any Noop Writes or Read locks early - [&] { - if constexpr (is_write_allowed()) { - if (!this->instance.template BitsetHas(this->writeAccessedFlags)) { - this->instance.template Storage().WriteUnlock(); + [this] { + if (instance.template BitsetHas(writePermissions)) { + if (!instance.template BitsetHas(writeAccessedFlags)) { + instance.template Storage().WriteUnlock(); } - } else if constexpr (is_read_allowed()) { - this->instance.template Storage().ReadUnlock(); + } else if (instance.template BitsetHas(readPermissions)) { + instance.template Storage().ReadUnlock(); } }(), ...); @@ -217,15 +263,13 @@ namespace Tecs { #ifdef TECS_ENABLE_TRACY ZoneNamedN(tracyCommitScope1, "CommitLock", true); #endif - if constexpr (is_add_remove_allowed()) { - if (this->writeAccessedFlags[0]) this->instance.metadata.CommitLock(); + if (writeAccessedFlags[0]) { // If AddRemove performed + instance.metadata.CommitLock(); } ( // For each AllComponentTypes - [&] { - if constexpr (is_write_allowed()) { - if (this->instance.template BitsetHas(this->writeAccessedFlags)) { - this->instance.template Storage().CommitLock(); - } + [this] { + if (instance.template BitsetHas(writeAccessedFlags)) { + instance.template Storage().CommitLock(); } }(), ...); @@ -234,33 +278,28 @@ namespace Tecs { #ifdef TECS_ENABLE_TRACY ZoneNamedN(tracyCommitScope2, "Commit", true); #endif - if constexpr (is_add_remove_allowed()) { - if (this->writeAccessedFlags[0]) { - // Commit observers - std::apply( - [](auto &...args) { - (args.Commit(), ...); - }, - this->instance.eventLists); - - this->instance.metadata.readComponents.swap(this->instance.metadata.writeComponents); - this->instance.metadata.readValidEntities.swap(this->instance.metadata.writeValidEntities); - this->instance.globalReadMetadata = this->instance.globalWriteMetadata; - this->instance.metadata.CommitUnlock(); - } + if (writeAccessedFlags[0]) { // If AddRemove performed + // Commit observers + std::apply( + [](auto &...args) { + (args.Commit(), ...); + }, + instance.eventLists); + + instance.metadata.readComponents.swap(instance.metadata.writeComponents); + instance.metadata.readValidEntities.swap(instance.metadata.writeValidEntities); + instance.globalReadMetadata = instance.globalWriteMetadata; + instance.metadata.CommitUnlock(); } - ( // For each AllComponentTypes - [&] { - if constexpr (is_write_allowed()) { - // Skip if no write accesses were made - if (!this->instance.template BitsetHas(this->writeAccessedFlags)) return; - auto &storage = this->instance.template Storage(); + ( // For each modified component type, swap read and write storage, and release commit lock. + [this] { + using CompType = AllComponentTypes; + if (instance.template BitsetHas(writeAccessedFlags)) { + auto &storage = instance.template Storage(); storage.readComponents.swap(storage.writeComponents); - if constexpr (is_add_remove_allowed()) { - if (this->writeAccessedFlags[0]) { - storage.readValidEntities.swap(storage.writeValidEntities); - } + if (writeAccessedFlags[0]) { // If AddRemove performed + storage.readValidEntities.swap(storage.writeValidEntities); } storage.CommitUnlock(); } @@ -268,16 +307,15 @@ namespace Tecs { ...); } - ( // For each AllComponentTypes, reset the write storage to match read. - [&] { - if constexpr (is_write_allowed()) { - // Skip if no write accesses were made - if (!this->instance.template BitsetHas(this->writeAccessedFlags)) return; - auto &storage = this->instance.template Storage(); + ( // For each modified component type, reset the write storage to match read. + [this] { + using CompType = AllComponentTypes; + if (instance.template BitsetHas(writeAccessedFlags)) { + auto &storage = instance.template Storage(); - if constexpr (is_global_component()) { + if (is_global_component()) { storage.writeComponents = storage.readComponents; - } else if (is_add_remove_allowed() && this->writeAccessedFlags[0]) { + } else if (writeAccessedFlags[0]) { // If AddRemove performed storage.writeComponents = storage.readComponents; storage.writeValidEntities = storage.readValidEntities; } else { @@ -295,21 +333,24 @@ namespace Tecs { } }(), ...); - if constexpr (is_add_remove_allowed()) { - if (this->writeAccessedFlags[0]) { - this->instance.metadata.writeComponents = this->instance.metadata.readComponents; - this->instance.metadata.writeValidEntities = this->instance.metadata.readValidEntities; + + if (writePermissions[0]) { // If AddRemove allowed + if (writeAccessedFlags[0]) { // If AddRemove performed + instance.metadata.writeComponents = instance.metadata.readComponents; + instance.metadata.writeValidEntities = instance.metadata.readValidEntities; } - } - if constexpr (is_add_remove_allowed()) { - this->instance.metadata.WriteUnlock(); + instance.metadata.WriteUnlock(); } else { - this->instance.metadata.ReadUnlock(); + instance.metadata.ReadUnlock(); } #ifdef TECS_ENABLE_PERFORMANCE_TRACING - TECS_EXTERNAL_TRACE_TRANSACTION_ENDED(FlatPermissions::Name()); - this->instance.transactionTrace.Trace(TraceEvent::Type::TransactionEnd); + // TECS_EXTERNAL_TRACE_TRANSACTION_ENDED(FlatPermissions::Name()); + instance.transactionTrace.Trace(TraceEvent::Type::TransactionEnd); +#endif +#ifndef TECS_HEADER_ONLY + auto start = activeTransactions.begin(); + activeTransactionsCount = std::remove(start, start + activeTransactionsCount, instance.ecsId) - start; #endif } @@ -318,30 +359,29 @@ namespace Tecs { inline void PreCommitAddRemoveMetadata() const { // Rebuild writeValidEntities, validEntityIndexes, and freeEntities with the new entity set. - this->instance.metadata.writeValidEntities.clear(); - this->instance.freeEntities.clear(); + instance.metadata.writeValidEntities.clear(); + instance.freeEntities.clear(); - const auto &writeMetadataList = this->instance.metadata.writeComponents; + const auto &writeMetadataList = instance.metadata.writeComponents; for (TECS_ENTITY_INDEX_TYPE index = 0; index < writeMetadataList.size(); index++) { const auto &newMetadata = writeMetadataList[index]; - const auto &oldMetadata = index >= this->instance.metadata.readComponents.size() + const auto &oldMetadata = index >= instance.metadata.readComponents.size() ? emptyMetadata - : this->instance.metadata.readComponents[index]; + : instance.metadata.readComponents[index]; // If this index exists, add it to the valid entity lists. if (newMetadata[0]) { - this->instance.metadata.validEntityIndexes[index] = - this->instance.metadata.writeValidEntities.size(); - this->instance.metadata.writeValidEntities.emplace_back(index, newMetadata.generation); + instance.metadata.validEntityIndexes[index] = instance.metadata.writeValidEntities.size(); + instance.metadata.writeValidEntities.emplace_back(index, newMetadata.generation); } else { - this->instance.freeEntities.emplace_back(index, + instance.freeEntities.emplace_back(index, newMetadata.generation + 1, - (TECS_ENTITY_ECS_IDENTIFIER_TYPE)this->instance.ecsId); + (TECS_ENTITY_ECS_IDENTIFIER_TYPE)instance.ecsId); } // Compare new and old metadata to notify observers if (newMetadata[0] != oldMetadata[0] || newMetadata.generation != oldMetadata.generation) { - auto &observerList = this->instance.template Observers(); + auto &observerList = instance.template Observers(); if (oldMetadata[0]) { observerList.writeQueue->emplace_back(EventType::REMOVED, Entity(index, oldMetadata.generation)); @@ -356,46 +396,46 @@ namespace Tecs { template inline void PreCommitAddRemove() const { if constexpr (is_global_component()) { - const auto &oldMetadata = this->instance.globalReadMetadata; - const auto &newMetadata = this->instance.globalWriteMetadata; - if (this->instance.template BitsetHas(newMetadata)) { - if (!this->instance.template BitsetHas(oldMetadata)) { - auto &observerList = this->instance.template Observers>(); + const auto &oldMetadata = instance.globalReadMetadata; + const auto &newMetadata = instance.globalWriteMetadata; + if (instance.template BitsetHas(newMetadata)) { + if (!instance.template BitsetHas(oldMetadata)) { + auto &observerList = instance.template Observers>(); observerList.writeQueue->emplace_back(EventType::ADDED, Entity(), - this->instance.template Storage().writeComponents[0]); + instance.template Storage().writeComponents[0]); } - } else if (this->instance.template BitsetHas(oldMetadata)) { - auto &observerList = this->instance.template Observers>(); + } else if (instance.template BitsetHas(oldMetadata)) { + auto &observerList = instance.template Observers>(); observerList.writeQueue->emplace_back(EventType::REMOVED, Entity(), - this->instance.template Storage().readComponents[0]); + instance.template Storage().readComponents[0]); } } else { - auto &storage = this->instance.template Storage(); + auto &storage = instance.template Storage(); // Rebuild writeValidEntities and validEntityIndexes with the new entity set. storage.writeValidEntities.clear(); - const auto &writeMetadataList = this->instance.metadata.writeComponents; + const auto &writeMetadataList = instance.metadata.writeComponents; for (TECS_ENTITY_INDEX_TYPE index = 0; index < writeMetadataList.size(); index++) { const auto &newMetadata = writeMetadataList[index]; - const auto &oldMetadata = index >= this->instance.metadata.readComponents.size() + const auto &oldMetadata = index >= instance.metadata.readComponents.size() ? emptyMetadata - : this->instance.metadata.readComponents[index]; + : instance.metadata.readComponents[index]; // If this index exists, add it to the valid entity lists. - if (newMetadata[0] && this->instance.template BitsetHas(newMetadata)) { + if (newMetadata[0] && instance.template BitsetHas(newMetadata)) { storage.validEntityIndexes[index] = storage.writeValidEntities.size(); storage.writeValidEntities.emplace_back(index, newMetadata.generation); } // Compare new and old metadata to notify observers - bool newExists = this->instance.template BitsetHas(newMetadata); - bool oldExists = this->instance.template BitsetHas(oldMetadata); + bool newExists = instance.template BitsetHas(newMetadata); + bool oldExists = instance.template BitsetHas(oldMetadata); if (newExists != oldExists || newMetadata.generation != oldMetadata.generation) { - auto &observerList = this->instance.template Observers>(); + auto &observerList = instance.template Observers>(); if (oldExists) { observerList.writeQueue->emplace_back(EventType::REMOVED, Entity(index, oldMetadata.generation), @@ -410,5 +450,13 @@ namespace Tecs { } } } + + template + friend class Lock; + template + friend class DynamicLock; + template + friend class EntityLock; + friend struct Entity; }; }; // namespace Tecs diff --git a/tests/tests.cpp b/tests/tests.cpp index 99f5cb0..f7cd7ee 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -7,12 +7,13 @@ #include #include #include -#include #include #include #include +#include using namespace testing; +using namespace std::string_literals; static ECS ecs; @@ -170,10 +171,6 @@ int main(int /* argc */, char ** /* argv */) { e.Set