From 953d906fa8b7ac079e4ce90f89963a1d42ff124d Mon Sep 17 00:00:00 2001 From: Octavian Dima <3403191+wopss@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:23:14 +0200 Subject: [PATCH] Add some updates to allocators and containers --- CMakeLists.txt | 3 +- examples/accessing_properties/Main.cpp | 135 +++ include/RED4ext/Common.hpp | 10 + include/RED4ext/Containers/DynArray.hpp | 790 ++++++++++++++++++ .../RED4ext/Detail/Containers/DynArray.hpp | 115 +++ .../Detail/Containers/DynArrayIterator.hpp | 134 +++ include/RED4ext/Detail/Memory.hpp | 47 +- include/RED4ext/Detail/Pool.hpp | 0 include/RED4ext/Exception.hpp | 17 + include/RED4ext/Memory/Allocator.hpp | 352 ++++++++ include/RED4ext/Memory/Pool.hpp | 13 +- 11 files changed, 1608 insertions(+), 8 deletions(-) create mode 100644 include/RED4ext/Containers/DynArray.hpp create mode 100644 include/RED4ext/Detail/Containers/DynArray.hpp create mode 100644 include/RED4ext/Detail/Containers/DynArrayIterator.hpp create mode 100644 include/RED4ext/Detail/Pool.hpp create mode 100644 include/RED4ext/Exception.hpp create mode 100644 include/RED4ext/Memory/Allocator.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f2e671595..b486dd232 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,11 +13,12 @@ project( # ----------------------------------------------------------------------------- if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 20) - set(CMAKE_CXX_STANDARD_REQUIRED ON) elseif(CMAKE_CXX_STANDARD LESS 20) message(FATAL_ERROR "RED4ext.SDK requires C++20 or higher.") endif() +set(CMAKE_CXX_STANDARD_REQUIRED ON) + if(PROJECT_IS_TOP_LEVEL) set_property(GLOBAL PROPERTY USE_FOLDERS ON) endif() diff --git a/examples/accessing_properties/Main.cpp b/examples/accessing_properties/Main.cpp index c6273974a..2d10287ec 100644 --- a/examples/accessing_properties/Main.cpp +++ b/examples/accessing_properties/Main.cpp @@ -111,8 +111,143 @@ void PrintScannerStatus(RED4ext::IScriptable* aContext, RED4ext::CStackFrame* aF } } +#include + RED4EXT_C_EXPORT void RED4EXT_CALL RegisterTypes() { + // https://github.com/oneapi-src/oneTBB/blob/master/include/oneapi/tbb/memory_pool.h + // https://stackoverflow.com/questions/826569/compelling-examples-of-custom-c-allocators + // https://stackoverflow.com/questions/657783/how-does-intel-tbbs-scalable-allocator-work + + { + std::vector*/> vector; + vector.clear(); + std::cout << vector.size() << std::endl; + + std::uninitialized_copy(vector.begin(), vector.end(), vector.data()); + std::uninitialized_move(vector.begin(), vector.end(), vector.data()); + } + + { + RED4ext::Containers::DynArray> + DynArrayDefaultAlloc; + + constexpr auto max_size = DynArrayDefaultAlloc.MaxSize(); + std::cout << max_size << std::endl; + + auto capacity = DynArrayDefaultAlloc.Capacity(); + std::cout << capacity << std::endl; + + DynArrayDefaultAlloc.Reserve(1025); + + capacity = DynArrayDefaultAlloc.Capacity(); + std::cout << capacity << std::endl; + + for (auto i : DynArrayDefaultAlloc) + { + std::cout << i << " "; + } + std::cout << std::endl; + + for (const auto i : DynArrayDefaultAlloc) + { + std::cout << i << " "; + } + std::cout << std::endl; + + for (auto& i : DynArrayDefaultAlloc) + { + std::cout << i << " "; + } + std::cout << std::endl; + + for (const auto& i : DynArrayDefaultAlloc) + { + std::cout << i << " "; + } + std::cout << std::endl; + } + + { + RED4ext::Containers::DynArray DynArray({1, 2, 3}); + } + + { + RED4ext::Containers::DynArray DynArray1({1, 2, 3}); + RED4ext::Containers::DynArray DynArray2(DynArray1); + } + + { + RED4ext::Containers::DynArray DynArray1({1, 2, 3}); + RED4ext::Containers::DynArray DynArray2(std::move(DynArray1)); + } + + { + RED4ext::Containers::DynArray DynArray(1); + } + + { + RED4ext::Containers::DynArray DynArray(1, 123); + } + + { + RED4ext::Containers::DynArray DynArray(1, 123); + DynArray.ShrinkToFit(); + DynArray.Clear(); + DynArray.PopBack(); + DynArray.Resize(10); + DynArray.Resize(123, 4); + DynArray.PushBack(1); + DynArray.PushBack(std::move(2)); + DynArray.EmplaceBack(3); + DynArray.Assign(3, 5); + DynArray.Erase(DynArray.begin()); + DynArray.Erase(DynArray.begin(), DynArray.end()); + } + + { + RED4ext::Containers::DynArray DynArray; + DynArray.ShrinkToFit(); + DynArray.Clear(); + DynArray.PopBack(); + } + + { + RED4ext::Allocator allocator; + auto allocate = allocator.Allocate(12); + auto a = allocator.Reallocate(allocate, 1234); + allocator.Deallocate(a); + + auto allocateAtLeast = allocator.AllocateAtLeast(255); + allocator.Deallocate(allocateAtLeast); + } + + { + constexpr RED4ext::Allocator allocator; + constexpr RED4ext::Allocator allocator2; + constexpr RED4ext::Allocator allocator3; + + { + constexpr auto equal = (allocator == allocator2); + std::cout << equal << std::endl; + } + + { + constexpr auto equal = (allocator == allocator3); + std::cout << equal << std::endl; + } + } + + { + std::allocator allocator; + auto a = allocator.allocate(12); + } + + { + // std::allocator_traits> triats; + // std::allocator_traits>::select_on_container_copy_construction triats.allocate(10); + } } RED4EXT_C_EXPORT void RED4EXT_CALL PostRegisterTypes() diff --git a/include/RED4ext/Common.hpp b/include/RED4ext/Common.hpp index 47ae4f3bb..c166d9875 100644 --- a/include/RED4ext/Common.hpp +++ b/include/RED4ext/Common.hpp @@ -2,6 +2,16 @@ #include +#if defined(_MSVC_LANG) && _MSVC_LANG > __cplusplus +#define RED4EXT_CPLUSPLUS _MSVC_LANG +#else +#define RED4EXT_CPLUSPLUS __cplusplus +#endif + +#ifndef RED4EXT_HAS_CPP23 +#define RED4EXT_HAS_CPP23 (RED4EXT_CPLUSPLUS > 202002L) +#endif + #ifdef RED4EXT_STATIC_LIB #undef RED4EXT_HEADER_ONLY #define RED4EXT_INLINE diff --git a/include/RED4ext/Containers/DynArray.hpp b/include/RED4ext/Containers/DynArray.hpp new file mode 100644 index 000000000..b813a944a --- /dev/null +++ b/include/RED4ext/Containers/DynArray.hpp @@ -0,0 +1,790 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace RED4ext +{ +namespace Containers +{ +template> +requires Detail::IsAllocator_New +struct DynArray +{ + using AllocatorType = typename std::allocator_traits::template rebind_alloc; + using AllocatorTraits = std::allocator_traits; + + using ValueType = T; + using Reference = T&; + using ConstReference = const T&; + using Pointer = typename AllocatorTraits::pointer; + using ConstPointer = typename AllocatorTraits::const_pointer; + + using SizeType = std::uint32_t; + using DifferenceType = typename AllocatorTraits::difference_type; + + using Iterator = Detail::DynArrayIterator; + using ConstIterator = Detail::DynArrayIterator; + using ReverseIterator = std::reverse_iterator; + using ConstReverseIterator = std::reverse_iterator; + + /* + * Future-proof assert. + * + * The game stores the allocator as the last entry in the "entries" field (entries[capacity]). The DynArray / + * DynamicBuffer always allocate capacity to fit this allocator. + * + * But, I cannot figure out how CDPR pass their allocators in constructor, thus our class will use a placement + * new to store the allocator in the "entries" pointer. + * + * From what I see the size of the allocators is always 8. This assert will make sure that the size of the + * allocators used in the template parameter will never be higher than of "RED4ext::Allocator" to prevent + * undefined behavior (e.g. constructing the allocator and overriding some unrelated memory). + * + * We could allocate more memory to fit a custom allocator (sizeof(TAllocator) > 8) but I saw that the functions + * related to the DynArray are always using 8 bytes for the allocator. + */ + static_assert(sizeof(AllocatorType) == sizeof(Allocator), + "The size of the specified allocator must be equal with the size of 'RED4ext::Allocator'"); + + DynArray() noexcept + : DynArray(AllocatorType()) + { + } + + explicit DynArray(const AllocatorType& aAllocator) noexcept + : entries(nullptr) + , size(0) + , capacity(0) + { + SetAllocator(aAllocator); + } + + explicit DynArray(SizeType aCount, const AllocatorType& aAllocator = AllocatorType()) + : DynArray(aAllocator) + { + Construct(aCount, std::uninitialized_value_construct_n, begin(), aCount); + } + + DynArray(SizeType aCount, const T& aValue, const AllocatorType& aAllocator = AllocatorType()) + : DynArray(aAllocator) + { + Construct(aCount, std::uninitialized_fill_n, begin(), aCount, aValue); + } + + template + requires std::input_iterator DynArray(InputIt aFirst, InputIt aLast, + const AllocatorType& aAllocator = AllocatorType()) + : DynArray(aAllocator) + { + using itCategory = typename std::iterator_traits::iterator_category; + if constexpr (std::is_convertible_v) + { + const auto newCapacity = static_cast(std::distance(aFirst, aLast)); + Construct(newCapacity, std::uninitialized_copy, aFirst, aLast, begin()); + } + else + { + for (; aFirst != aLast; ++aFirst) + { + EmplaceBack(*aFirst); + } + } + } + + DynArray(std::initializer_list aInit, const AllocatorType& aAllocator = AllocatorType()) + : DynArray(aInit.begin(), aInit.end(), aAllocator) + { + } + + DynArray(const DynArray& aOther) + : DynArray(aOther, AllocatorTraits::select_on_container_copy_construction(aOther.GetAllocator())) + { + } + + DynArray(const DynArray& aOther, const AllocatorType& aAllocator) + : DynArray(aOther.begin(), aOther.end(), aAllocator) + { + } + + DynArray(DynArray&& aOther) noexcept + : DynArray(std::move(aOther.GetAllocator())) + { + Swap(aOther); + } + + DynArray(DynArray&& aOther, const AllocatorType& aAllocator) + : DynArray(aAllocator) + { + if (aAllocator == GetAllocator()) + { + Swap(aOther); + } + else + { + const auto otherSize = aOther.Size(); + Construct(otherSize, std::uninitialized_move, aOther.begin(), aOther.end(), begin()); + } + } + + DynArray& operator=(const DynArray& aRhs) noexcept + { + if (this != std::addressof(aRhs)) + { + // https://www.foonathan.net/2015/10/allocatorawarecontainer-propagation-pitfalls/ + } + + return *this; + } + + DynArray& operator=(DynArray&& aRhs) noexcept + { + if (this != std::addressof(aRhs)) + { + // https://www.foonathan.net/2015/10/allocatorawarecontainer-propagation-pitfalls/ + } + + return *this; + } + + DynArray& operator=(std::initializer_list aInit) noexcept + { + Assign(aInit); + return *this; + } + + ~DynArray() + { + DestroyAndDeallocate(); + } + + void Assign(SizeType aCount, const T& aValue) noexcept + { + if (Capacity() < aCount) + { + DynArray tmp(aCount, aValue, GetAllocator()); + Swap(tmp); + } + else if (Size() < aCount) + { + std::fill(begin(), end(), aValue); + + const SizeType delta = aCount - Size(); + std::uninitialized_fill_n(end(), delta, aValue); + + size = aCount; + } + else + { + auto it = begin(); + std::fill_n(it, aCount, aValue); + + std::advance(it, aCount); + Erase(it, end()); + + size = aCount; + } + } + + //////////////////////////////////////////////////////////////////////////////////////// + + template + requires std::input_iterator + void Assign(InputIt aFirst, InputIt aLast) noexcept + { + // TODO: Implement this. + } + + //////////////////////////////////////////////////////////////////////////////////////// + + void Assign(std::initializer_list aInit) noexcept + { + Assign(aInit.begin(), aInit.end()); + } + + [[nodiscard]] Reference operator[](SizeType aIndex) noexcept + { + assert(aIndex < Size()); + return Data()[aIndex]; + } + + [[nodiscard]] ConstReference operator[](SizeType aIndex) const noexcept + { + assert(aIndex < Size()); + return Data()[aIndex]; + } + + [[nodiscard]] Reference At(SizeType aIndex) noexcept + { + if (aIndex >= Size()) + { + throw Exception("DynArray::at: out of range"); + } + + return Data()[aIndex]; + } + + [[nodiscard]] ConstReference At(SizeType aIndex) const noexcept + { + if (aIndex >= Size()) + { + throw Exception("DynArray::at: out of range"); + } + + return Data()[aIndex]; + } + + [[nodiscard]] Reference Front() noexcept + { + assert(!Empty()); + return Data()[0]; + } + + [[nodiscard]] ConstReference Front() const noexcept + { + assert(!Empty()); + return Data()[0]; + } + + [[nodiscard]] Reference Back() noexcept + { + assert(!Empty()); + return Data()[Size() - 1]; + } + + [[nodiscard]] ConstReference Back() const noexcept + { + assert(!Empty()); + return Data()[Size() - 1]; + } + + [[nodiscard]] T* Data() noexcept + { + return entries; + } + + [[nodiscard]] const T* Data() const noexcept + { + return entries; + } + + [[nodiscard]] bool Empty() const noexcept + { + return Size() == 0; + } + + [[nodiscard]] SizeType Size() const noexcept + { + return size; + } + + [[nodiscard]] constexpr SizeType MaxSize() const noexcept + { + constexpr size_t typeSize = sizeof(ValueType); + constexpr size_t allocatorSize = AlignUp(sizeof(AllocatorType), size_t(4)); + constexpr SizeType maxSize = (std::numeric_limits::max)() / typeSize; + + // Make sure that the maximum size can fit the allocator at the end of the data. + if constexpr (typeSize < allocatorSize) + { + constexpr auto alignedTypeSize = (std::max)(size_t(1), AlignDown(typeSize, size_t(4))); + return maxSize - (allocatorSize / alignedTypeSize); + } + else + { + return maxSize - 1; + } + } + + [[nodiscard]] SizeType Capacity() const noexcept + { + return capacity; + } + + void Reserve(SizeType aNewCapacity) + { + if (MaxSize() < aNewCapacity) + { + throw Exception("DynArray::Reserve: too many elements"); + } + + if (Capacity() < aNewCapacity) + { + GrowExactly(aNewCapacity); + } + } + + void ShrinkToFit() + { + if (Capacity() > Size()) + { + DynArray tmp(std::make_move_iterator(begin()), std::make_move_iterator(end()), GetAllocator()); + Swap(tmp); + } + } + + void Clear() noexcept + { + std::destroy(begin(), end()); + size = 0; + } + + Iterator Insert(ConstIterator aPos, const T& aValue) + { + Emplace(aPos, aValue); + } + + Iterator Insert(ConstIterator aPos, T&& aValue) + { + Emplace(aPos, std::move(aValue)); + } + + //////////////////////////////////////////////////////////////////////////////////////// + Iterator Insert(ConstIterator aPos, SizeType aCount, const T& aValue) + { + // TODO: Implement this. + return {}; + } + + template + requires std::input_iterator Iterator Insert(ConstIterator aPos, InputIt aFirst, InputIt aLast) + { + // TODO: Implement this. + return {}; + } + + //////////////////////////////////////////////////////////////////////////////////////// + + Iterator Insert(ConstIterator aPos, std::initializer_list aList) + { + return Insert(aPos, aList.begin(), aList.end()); + } + + template + Iterator Emplace(ConstIterator aPos, Args&&... aArgs) + { + // Saving this because we might relocate. + auto distance = std::distance(cbegin(), aPos); + + if (aPos == cend()) + { + EmplaceBack(std::forward(aArgs)...); + } + else if (Size() < Capacity()) + { + ValueType value(std::forward(aArgs)...); + + auto endIt = end(); + auto penultimateIt = std::prev(endIt); + + auto allocator = GetAllocator(); + AllocatorTraits::construct(allocator, std::addressof(*endIt), std::move(*penultimateIt)); + + Iterator whereIt(aPos.Base()); + + std::move_backward(whereIt, penultimateIt, endIt); + AllocatorTraits::destroy(allocator, std::addressof(*whereIt)); + + AllocatorTraits::construct(allocator, std::addressof(*whereIt), std::move(value)); + ++size; + } + else + { + // TODO: Relocate. + } + + return std::next(begin(), distance); + } + + Iterator Erase(ConstIterator aPos) + { + Iterator whereIt(aPos.Base()); + auto endIt = end(); + + std::move(std::next(whereIt), endIt, whereIt); + + auto allocator = GetAllocator(); + auto penultimateIt = std::prev(endIt); + + AllocatorTraits::destroy(allocator, std::addressof(*penultimateIt)); + --size; + + return Iterator(aPos.Base()); + } + + Iterator Erase(ConstIterator aFirst, ConstIterator aLast) + { + if (aFirst != aLast) + { + Iterator firstIt(aFirst.Base()); + Iterator lastIt(aLast.Base()); + auto endIt = end(); + + auto pos = std::move(lastIt, endIt, firstIt); + std::destroy(pos, endIt); + + size -= static_cast(std::distance(aFirst, aLast)); + } + + return Iterator(aFirst.Base()); + } + + void PushBack(const ValueType& aValue) + { + EmplaceBack(aValue); + } + + void PushBack(ValueType&& aValue) + { + EmplaceBack(std::move(aValue)); + } + + template + Reference EmplaceBack(Args&&... aArgs) + { + if (Size() < Capacity()) + { + auto allocator = GetAllocator(); + auto whereIt = end(); + + AllocatorTraits::construct(allocator, std::addressof(*whereIt), std::forward(aArgs)...); + ++size; + } + else + { + // TODO: Relocate. + } + + return Back(); + + // TODO: Can use insert here? + /*Emplace(end(), std::forward(aArgs)...); + return Back();*/ + + // if (Size() < Capacity()) + //{ + // auto& back = Back(); + + // auto allocator = GetAllocator(); + // AllocatorTraits::construct(allocator, std::addressof(back), std::forward(aArgs)...); + //} + // else + //{ + // // TODO: Insert. + //} + + // return Back(); + + // if (Size() >= Capacity()) + //{ + // Grow(Capacity() + 1); + // size++; + // } + + // auto& back = Back(); + + // auto allocator = GetAllocator(); + // AllocatorTraits::construct(allocator, std::addressof(back), std::forward(aArgs)...); + + // return back; + } + + void PopBack() + { + assert(!Empty()); + + auto& last = Back(); + auto allocator = GetAllocator(); + + AllocatorTraits::destroy(allocator, std::addressof(last)); + --size; + } + + void Resize(SizeType aCount) + { + ResizeAndAppend(aCount, std::uninitialized_default_construct_n); + } + + void Resize(SizeType aCount, const ValueType& aValue) + { + ResizeAndAppend(aCount, std::uninitialized_fill_n, aValue); + } + + void Swap(DynArray& aOther) noexcept + { + if (this != std::addressof(aOther)) + { + // Note: swapping two containers with unequal allocators if propagate_on_container_swap is false is + // undefined behavior. (https://en.cppreference.com/w/cpp/named_req/AllocatorAwareContainer) + + std::swap(entries, aOther.entries); + std::swap(size, aOther.size); + std::swap(capacity, aOther.capacity); + + // TODO: See https://www.foonathan.net/2015/10/allocatorawarecontainer-propagation-pitfalls/. + } + } + +#pragma region STL + Iterator begin() noexcept + { + return Iterator(entries); + } + + ConstIterator begin() const noexcept + { + return ConstIterator(entries); + } + + ConstIterator cbegin() const noexcept + { + return ConstIterator(entries); + } + + Iterator end() noexcept + { + return Iterator(entries + size); + } + + ConstIterator end() const noexcept + { + return ConstIterator(entries + size); + } + + ConstIterator cend() const noexcept + { + return ConstIterator(entries + size); + } + + ReverseIterator rbegin() noexcept + { + return ReverseIterator(begin()); + } + + ConstReverseIterator rbegin() const noexcept + { + return ConstReverseIterator(begin()); + } + + ConstReverseIterator crbegin() const noexcept + { + return ConstReverseIterator(cbegin()); + } + + ReverseIterator rend() noexcept + { + return ReverseIterator(end()); + } + + ConstReverseIterator rend() const noexcept + { + return ConstReverseIterator(end()); + } + + ConstReverseIterator crend() const noexcept + { + return ConstReverseIterator(cend()); + } +#pragma endregion + + [[nodiscard]] AllocatorType GetAllocator() const noexcept + { + if (!entries) + { + return {}; + } + + return *reinterpret_cast(&entries[capacity]); + } + + bool operator==(const DynArray& aRhs) + { + if (Size() != aRhs.Size()) + { + return false; + } + + return std::equal(begin(), end(), aRhs.begin()); + } + + auto operator<=>(const DynArray& aRhs) + { + return std::lexicographical_compare_three_way(begin(), end(), aRhs.begin(), aRhs.end()); + } + + T* entries; // 00 + std::uint32_t capacity; // 08 + std::uint32_t size; // 0C + +private: + void SetAllocator(const AllocatorType& aAllocator) + { + auto location = reinterpret_cast(std::addressof(entries)); + if (entries) + { + location = reinterpret_cast(std::addressof(entries[capacity])); + } + + std::construct_at(location, aAllocator); + } + + Pointer Allocate(SizeType aObjects) + { + if (aObjects > MaxSize()) + { + throw Exception("DynArray::Allocate: too many elements"); + } + + if (aObjects) + { + const auto allocator = GetAllocator(); + const auto sizeWithAllocator = aObjects + 1; + + return allocator.Allocate(sizeWithAllocator, std::alignment_of_v); + } + + return nullptr; + } + + void Deallocate(Pointer aMemory) + { + if (aMemory) + { + const auto allocator = GetAllocator(); + allocator.Deallocate(aMemory); + } + } + + template + requires std::invocable + void Construct(SizeType aCount, F aFunc, Args&&... aArgs) + { + if (aCount) + { + Reserve(aCount); + aFunc(std::forward(aArgs)...); + size = aCount; + } + } + + void DestroyAndDeallocate() + { + if (entries) + { + std::destroy(begin(), end()); + Deallocate(entries); + + entries = nullptr; + } + } + + void ChangeArray(const Pointer aNewPtr, const SizeType aNewSize, const SizeType aNewCapacity) + { + // Make a copy of the allocator. + const auto allocator = GetAllocator(); + + DestroyAndDeallocate(); + + entries = aNewPtr; + size = aNewSize; + capacity = aNewCapacity; + + // Set the allocator for the new ptr. + SetAllocator(allocator); + } + + SizeType CalculateGrowth(SizeType aNewSize) + { + const SizeType maxSize = MaxSize(); + const SizeType currentCapacity = Capacity(); + const SizeType maxCapacity = maxSize - currentCapacity / 2; + + if (currentCapacity > maxCapacity) + { + // Overflow possible, just use the new size. + return maxSize; + } + + const SizeType geometric = currentCapacity + (currentCapacity / 2); + if (geometric < aNewSize) + { + return aNewSize; + } + + return geometric; + } + + void Grow(SizeType aNewCapacity) + { + auto growth = CalculateGrowth(aNewCapacity); + GrowExactly(growth); + } + + void GrowExactly(SizeType aNewCapacity) + { + Pointer newData = Allocate(aNewCapacity); + + try + { + Detail::UninitializedMoveIfNoexcept(begin(), end(), newData); + } + catch (...) + { + Deallocate(newData); + throw; + } + + ChangeArray(newData, Size(), aNewCapacity); + } + + template + requires std::invocable + void ResizeAndAppend(SizeType aCount, F aFunc, Args&&... aArgs) + { + if (Size() < aCount) + { + Iterator beginIt; + SizeType delta = aCount - Size(); + + if (Capacity() < aCount) + { + auto oldSize = Size(); + + Grow(aCount); + beginIt = begin() + oldSize; + } + else + { + beginIt = end(); + } + + aFunc(beginIt, delta, std::forward(aArgs)...); + size = aCount; + } + else if (Size() > aCount) + { + std::destroy(begin() + aCount, end()); + size = aCount; + } + } +}; +RED4EXT_ASSERT_SIZE(DynArray, 0x10); +RED4EXT_ASSERT_OFFSET(DynArray, entries, 0x00); +RED4EXT_ASSERT_OFFSET(DynArray, capacity, 0x08); +RED4EXT_ASSERT_OFFSET(DynArray, size, 0x0C); +} // namespace Containers +} // namespace RED4ext diff --git a/include/RED4ext/Detail/Containers/DynArray.hpp b/include/RED4ext/Detail/Containers/DynArray.hpp new file mode 100644 index 000000000..0cbb5e964 --- /dev/null +++ b/include/RED4ext/Detail/Containers/DynArray.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include + +namespace RED4ext::Detail +{ +template +class DynArrayIterator +{ +public: + using iterator_concept = std::contiguous_iterator_tag; + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using reference = T&; + using pointer = T*; + using difference_type = std::ptrdiff_t; + + constexpr DynArrayIterator(pointer aPtr) noexcept + : m_ptr(aPtr) + { + } + + constexpr DynArrayIterator(const DynArrayIterator&) noexcept = default; + constexpr DynArrayIterator(DynArrayIterator&&) noexcept = default; + + constexpr DynArrayIterator& operator=(const DynArrayIterator&) noexcept = default; + constexpr DynArrayIterator& operator=(DynArrayIterator&&) noexcept = default; + + constexpr ~DynArrayIterator() noexcept = default; + + [[nodiscard]] constexpr reference operator*() const noexcept + { + return *m_ptr; + } + + [[nodiscard]] constexpr pointer operator->() const noexcept + { + return m_ptr; + } + + constexpr DynArrayIterator& operator++() noexcept + { + ++m_ptr; + return *this; + } + + constexpr DynArrayIterator operator++(int) noexcept + { + auto tmp = *this; + ++(*this); + + return tmp; + } + + constexpr DynArrayIterator& operator--() noexcept + { + --m_ptr; + return *this; + } + + constexpr DynArrayIterator operator--(int) noexcept + { + auto tmp = *this; + --(*this); + + return tmp; + } + + [[nodiscard]] constexpr reference operator[](const difference_type aOffset) const noexcept + { + return m_ptr[aOffset]; + } + + constexpr DynArrayIterator& operator+=(const difference_type aOffset) noexcept + { + m_ptr += aOffset; + return *this; + } + + [[nodiscard]] constexpr DynArrayIterator operator+(const difference_type aOffset) const noexcept + { + return DynArrayIterator(m_ptr + aOffset); + } + + constexpr DynArrayIterator& operator-=(const difference_type aOffset) noexcept + { + m_ptr -= aOffset; + return *this; + } + + [[nodiscard]] constexpr DynArrayIterator operator-(const difference_type aOffset) const noexcept + { + return DynArrayIterator(m_ptr - aOffset); + } + + [[nodiscard]] constexpr difference_type operator-(const DynArrayIterator& aRhs) const noexcept + { + return m_ptr - aRhs.m_ptr; + } + + [[nodiscard]] constexpr bool operator==(const DynArrayIterator& aRhs) const noexcept + { + return m_ptr == aRhs.m_ptr; + } + + [[nodiscard]] constexpr auto operator<=>(const DynArrayIterator& aRhs) const noexcept + { + // TODO: _Unfancy + return m_ptr <=> aRhs.m_ptr; + } + +private: + pointer m_ptr; +}; +} // namespace RED4ext::Detail diff --git a/include/RED4ext/Detail/Containers/DynArrayIterator.hpp b/include/RED4ext/Detail/Containers/DynArrayIterator.hpp new file mode 100644 index 000000000..b72cbff52 --- /dev/null +++ b/include/RED4ext/Detail/Containers/DynArrayIterator.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include + +namespace RED4ext::Detail +{ +template +class DynArrayIterator +{ +public: + using iterator_concept = std::contiguous_iterator_tag; + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using reference = T&; + using pointer = T*; + using difference_type = std::ptrdiff_t; + + constexpr DynArrayIterator() noexcept + : m_ptr(nullptr) + { + } + + constexpr DynArrayIterator(Container::Pointer aPtr) noexcept + : m_ptr(aPtr) + { + } + + // Allow conversion from Iterator to ConstIterator. + template + requires std::same_as + constexpr DynArrayIterator(const DynArrayIterator& aOther) noexcept + : m_ptr(aOther.Base()) + { + } + + constexpr DynArrayIterator(const DynArrayIterator&) noexcept = default; + constexpr DynArrayIterator(DynArrayIterator&&) noexcept = default; + + constexpr DynArrayIterator& operator=(const DynArrayIterator&) noexcept = default; + constexpr DynArrayIterator& operator=(DynArrayIterator&&) noexcept = default; + + constexpr ~DynArrayIterator() noexcept = default; + + [[nodiscard]] constexpr reference operator*() const noexcept + { + return *m_ptr; + } + + [[nodiscard]] constexpr pointer operator->() const noexcept + { + return m_ptr; + } + + constexpr DynArrayIterator& operator++() noexcept + { + ++m_ptr; + return *this; + } + + constexpr DynArrayIterator operator++(int) noexcept + { + auto tmp = *this; + ++(*this); + + return tmp; + } + + constexpr DynArrayIterator& operator--() noexcept + { + --m_ptr; + return *this; + } + + constexpr DynArrayIterator operator--(int) noexcept + { + auto tmp = *this; + --(*this); + + return tmp; + } + + [[nodiscard]] constexpr reference operator[](const difference_type aOffset) const noexcept + { + return m_ptr[aOffset]; + } + + constexpr DynArrayIterator& operator+=(const difference_type aOffset) noexcept + { + m_ptr += aOffset; + return *this; + } + + [[nodiscard]] constexpr DynArrayIterator operator+(const difference_type aOffset) const noexcept + { + return DynArrayIterator(m_ptr + aOffset); + } + + constexpr DynArrayIterator& operator-=(const difference_type aOffset) noexcept + { + m_ptr -= aOffset; + return *this; + } + + [[nodiscard]] constexpr DynArrayIterator operator-(const difference_type aOffset) const noexcept + { + return DynArrayIterator(m_ptr - aOffset); + } + + [[nodiscard]] constexpr difference_type operator-(const DynArrayIterator& aRhs) const noexcept + { + return m_ptr - aRhs.m_ptr; + } + + [[nodiscard]] constexpr bool operator==(const DynArrayIterator& aRhs) const noexcept + { + return m_ptr == aRhs.m_ptr; + } + + [[nodiscard]] constexpr auto operator<=>(const DynArrayIterator& aRhs) const noexcept + { + // TODO: _Unfancy + return m_ptr <=> aRhs.m_ptr; + } + + [[nodiscard]] constexpr Container::Pointer Base() const noexcept + { + return m_ptr; + } + +private: + Container::Pointer m_ptr; +}; +} // namespace RED4ext::Detail diff --git a/include/RED4ext/Detail/Memory.hpp b/include/RED4ext/Detail/Memory.hpp index 3f86a5e35..ff8fa01c0 100644 --- a/include/RED4ext/Detail/Memory.hpp +++ b/include/RED4ext/Detail/Memory.hpp @@ -1,17 +1,23 @@ #pragma once -#include +#include +#include +#include #include namespace RED4ext { namespace Memory { +struct PoolInfo; struct IAllocator; -} +} // namespace Memory namespace Detail { +template +concept IsPointer = std::is_pointer_v; + template struct AddressResolverOverride : std::false_type { @@ -90,5 +96,42 @@ concept IsSafeDestructible = template concept IsAllocator = std::is_base_of_v; + +template::size_type, + typename alignment_type = T::alignment_type, typename allocation_result_type = T::allocation_result_type> +concept IsAllocator_New = requires(T t) +{ + // clang-format off + { t.AllocateAtLeast(std::declval()) } -> std::same_as; + { t.AllocateAtLeast(std::declval(), std::declval()) } -> std::same_as; + { t.ReallocateAtLeast(std::declval(), std::declval()) } -> std::same_as; + { t.ReallocateAtLeast(std::declval(), std::declval(), std::declval()) } -> std::same_as; + { t.Deallocate(std::declval()) } -> std::same_as; + { t.sub_28(std::declval()) } -> std::same_as; + { t.GetHandle() } -> std::same_as; + // clang-format on +}; + +template +concept IsMemoryPool = std::is_base_of_v && requires(T) +{ + // clang-format off + { T::Name } -> std::convertible_to; + // clang-format on +}; + +template +inline ForwardIt UninitializedMoveIfNoexcept(const InputIt aFirst, const InputIt aLast, ForwardIt aDest) +{ + using valueType = typename std::iterator_traits::value_type; + if constexpr (std::is_nothrow_move_constructible_v || !std::is_copy_constructible_v) + { + return std::uninitialized_move(aFirst, aLast, aDest); + } + else + { + return std::uninitialized_copy(aFirst, aLast, aDest); + } +} } // namespace Detail } // namespace RED4ext diff --git a/include/RED4ext/Detail/Pool.hpp b/include/RED4ext/Detail/Pool.hpp new file mode 100644 index 000000000..e69de29bb diff --git a/include/RED4ext/Exception.hpp b/include/RED4ext/Exception.hpp new file mode 100644 index 000000000..638133fe2 --- /dev/null +++ b/include/RED4ext/Exception.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace RED4ext +{ +class [[nodiscard]] Exception : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; + + Exception(const Exception&) noexcept = default; + Exception& operator=(const Exception&) noexcept = default; + + virtual ~Exception() noexcept = default; +}; +} // namespace RED4ext diff --git a/include/RED4ext/Memory/Allocator.hpp b/include/RED4ext/Memory/Allocator.hpp new file mode 100644 index 000000000..15aec7aa9 --- /dev/null +++ b/include/RED4ext/Memory/Allocator.hpp @@ -0,0 +1,352 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace RED4ext +{ +template +requires Detail::IsPointer +#if RED4EXT_HAS_CPP23 +using AllocationResult = std::allocation_result; +#else +struct [[nodiscard]] AllocationResult +{ + T ptr; + std::size_t count; +}; +#endif +RED4EXT_ASSERT_SIZE(AllocationResult, 0x10); +RED4EXT_ASSERT_OFFSET(AllocationResult, ptr, 0x0); +RED4EXT_ASSERT_OFFSET(AllocationResult, count, 0x8); + +template +requires Detail::IsMemoryPool +class Allocator +{ +public: + // TODO: See propagate_on_container_copy_assignment, propagate_on_container_move_assignment and + // propagate_on_container_swap. + using value_type = T; + using pool_type = Pool; + using size_type = std::size_t; + using alignment_type = std::uint32_t; + using difference_type = std::ptrdiff_t; + using allocation_result_type = AllocationResult; + + using propagate_on_container_move_assignment = std::true_type; + + static_assert( + !std::is_const_v, + "The C++ Standard forbids containers of const elements because RED4ext::Allocator is ill-formed."); + + constexpr Allocator() noexcept = default; + constexpr Allocator(const Allocator&) noexcept = default; + constexpr Allocator(Allocator&&) noexcept = default; + + template + constexpr Allocator(const Allocator&) noexcept + { + } + + constexpr Allocator& operator=(const Allocator&) noexcept = default; + constexpr Allocator& operator=(Allocator&&) noexcept = default; + + constexpr ~Allocator() noexcept = default; + + /* + * Not using function overloading because then it will be up to the compiler to decide how the vtbl is ordered. + * Since we want a specific ordering the preffix "WithAlignment" is added to the functions that contain the + * alignment parameter. + * + * Also all virtual [re]allocation functions are private. If someone wants to allocate exactly N bytes, then they + * can use Allocator. + */ + +private: + [[nodiscard]] virtual AllocationResult AllocateAtLeastBytes(size_type aBytes) const + { + static_assert(sizeof(value_type) > 0, + "The type must be complete before calling 'Allocator::AllocateAtLeastBytes'"); + return InvokeAllocationFunction(Addresses::Memory_Vault_Alloc, aBytes, + [this, aBytes]() { RiseOom(aBytes, 8); }); + } + + [[nodiscard]] virtual AllocationResult AllocateAtLeastBytesWithAlignment(size_type aBytes, + alignment_type aAlignment) const + { + static_assert(sizeof(value_type) > 0, + "The type must be complete before calling 'Allocator::AllocateAtLeastBytesWithAlignment'"); + return InvokeAllocationFunction( + Addresses::Memory_Vault_AllocAligned, aBytes, aAlignment, + [this, aBytes, aAlignment]() { RiseOom(aBytes, aAlignment); }); + } + + [[nodiscard]] virtual AllocationResult ReallocateAtLeastBytes( + const allocation_result_type& aPreviousAllocation, size_type aBytes) const + { + static_assert(sizeof(value_type) > 0, + "The type must be complete before calling 'Allocator::ReallocateAtLeastBytes'"); + return InvokeAllocationFunction(Addresses::Memory_Vault_Realloc, + aPreviousAllocation, aBytes, + [this, aBytes]() + { + if (aBytes) + { + RiseOom(aBytes, 8); + } + }); + } + + [[nodiscard]] virtual AllocationResult ReallocateAtLeastBytesWithAlignment( + const allocation_result_type& aPreviousAllocation, size_type aBytes, alignment_type aAlignment) const + { + static_assert(sizeof(value_type) > 0, + "The type must be complete before calling 'Allocator::ReallocateAtLeastBytesWithAlignment'"); + return InvokeAllocationFunction( + Addresses::Memory_Vault_ReallocAligned, aPreviousAllocation, aBytes, aAlignment, + [this, aBytes, aAlignment]() + { + if (aBytes) + { + RiseOom(aBytes, aAlignment); + } + }); + } + +public: + virtual void Deallocate(const allocation_result_type& aPreviousAllocation) const + { + InvokeGenericFunction(Addresses::Memory_Vault_Free, aPreviousAllocation); + } + + virtual void sub_28(void* a1) const + { + InvokeGenericFunction(Addresses::Memory_Vault_Unk1, a1); + } + + [[nodiscard]] virtual const std::uint32_t GetHandle() const + { + auto pool = pool_type::Get(); + return pool->handle; + } + + [[nodiscard]] T* Allocate(size_type aObjects) const + { + auto result = AllocateAtLeast(aObjects); + return result.ptr; + } + + [[nodiscard]] T* Allocate(size_type aObjects, alignment_type aAlignment) const + { + auto result = AllocateAtLeast(aObjects, aAlignment); + return result.ptr; + } + + [[nodiscard]] allocation_result_type AllocateAtLeast(size_type aObjects) const + { + auto size = CalculateByteCount(aObjects); + auto result = AllocateAtLeastBytes(size); + + return {static_cast(result.ptr), result.count}; + } + + [[nodiscard]] allocation_result_type AllocateAtLeast(size_type aObjects, alignment_type aAlignment) const + { + auto size = CalculateByteCount(aObjects); + auto result = AllocateAtLeastBytesWithAlignment(size, aAlignment); + + return {static_cast(result.ptr), result.count}; + } + + [[nodiscard]] T* Reallocate(T* aMemory, size_type aObjects) const + { + return Reallocate({aMemory, 0}, aObjects); + } + + [[nodiscard]] T* Reallocate(const allocation_result_type& aPreviousAllocation, size_type aObjects) const + { + auto result = ReallocateAtLeast(aPreviousAllocation, aObjects); + return result.ptr; + } + + [[nodiscard]] T* Reallocate(T* aMemory, size_type aObjects, alignment_type aAlignment) const + { + return Reallocate({aMemory, 0}, aObjects, aAlignment); + } + + [[nodiscard]] T* Reallocate(const allocation_result_type& aPreviousAllocation, size_type aObjects, + alignment_type aAlignment) const + { + auto result = ReallocateAtLeast(aPreviousAllocation, aObjects, aAlignment); + return result.ptr; + } + + [[nodiscard]] allocation_result_type ReallocateAtLeast(T* aMemory, size_type aObjects) const + { + return ReallocateAtLeast({aMemory, 0}, aObjects); + } + + [[nodiscard]] allocation_result_type ReallocateAtLeast(const allocation_result_type& aPreviousAllocation, + size_type aObjects) const + { + auto size = CalculateByteCount(aObjects); + auto result = ReallocateAtLeastBytes(aPreviousAllocation, size); + + return {static_cast(result.ptr), result.count}; + } + + [[nodiscard]] allocation_result_type ReallocateAtLeast(T* aMemory, size_type aObjects, + alignment_type aAlignment) const + { + return ReallocateAtLeast({aMemory, 0}, aObjects, aAlignment); + } + + [[nodiscard]] allocation_result_type ReallocateAtLeast(const allocation_result_type& aPreviousAllocation, + size_type aObjects, alignment_type aAlignment) const + { + auto size = CalculateByteCount(aObjects); + auto result = ReallocateAtLeastBytesWithAlignment(aPreviousAllocation, size, aAlignment); + + return {static_cast(result.ptr), result.count}; + } + + void Deallocate(T* aMemory) const + { + Deallocate({aMemory, 0}); + } + + void Deallocate(T* aMemory, size_type aObjects) const + { + Deallocate({aMemory, aObjects}); + } + +#pragma region STL + [[nodiscard]] T* allocate(size_type aObjects) + { + return Allocate(aObjects); + } + +#if RED4EXT_HAS_CPP23 + [[nodiscard]] std::allocation_result allocate_at_least(std::size_t aObjects) + { + return AllocateAtLeast(aObjects); + } +#endif + + void deallocate(T* aMemory, size_type aObjects) + { + Deallocate(aMemory, aObjects); + } +#pragma endregion + +private: + [[nodiscard]] static constexpr size_type CalculateByteCount(size_type aObjects) + { + constexpr size_t typeSize = sizeof(T); + constexpr size_type maxSize = (std::numeric_limits::max)() / typeSize; + + // Check if overflow is possible. + if (maxSize < aObjects) + { + throw Exception("bad array new length"); + } + + return aObjects * typeSize; + } + + [[nodiscard]] static pool_type* GetPool() + { + auto vault = Memory::Vault::Get(); + auto& poolRegistry = vault->poolRegistry; + + static auto pool = poolRegistry.Get(pool_type::Name); + return pool; + } + + [[nodiscard]] static Memory::Vault* GetAllocatorStorage() + { + const auto pool = GetPool(); + return pool->storage->GetAllocatorStorage(); + } + + /** + * @brief Invoke a generic function on the allocator's storage. + * + * @note Argument types are not using universal reference because it is necessary to control how the parameters are + * passed to the real function. We don't want unexpected lvalue references to be used. + */ + template + static void InvokeGenericFunction(std::uintptr_t aOffset, Args... aArgs) + { + auto storage = GetAllocatorStorage(); + + RelocFunc func(aOffset); + func(storage, std::forward(aArgs)...); + } + + /** + * @brief Invoke an allocation function on the allocator's storage. + * + * @note See notes of InvokeFunction. + */ + template + [[nodiscard]] static AllocationResult InvokeAllocationFunction(std::uintptr_t aOffset, Args... aArgs, + const std::function& aOomHandler) + { + auto storage = GetAllocatorStorage(); + + /* + * All [re]alloc functions have the following typedef: + * AllocationResult [Re]Alloc(Vault* aVault, [AllocationResult& aPreviousAllocation], uint32_t aSize, + * [uint32_t aAlignment]) + * + * Because "REDfunc" is using a variable to call them in memory we cannot abuse the __fastcall convention, + * thus it is necessary to use the following typedef: + * void [Re]Alloc(Vault* aVault, AllocationResult* aResult, [AllocationResult& aPreviousAllocation], + * uint32_t aSize, [uint32_t aAlignment]) + * + * Specifing the RDX directly and using void as return type should not cause problems when calling these + * functions, doing something else the calls will be ill-formed. + */ + RelocFunc&, Args...)> func(aOffset); + + AllocationResult result{}; + func(storage, result, std::forward(aArgs)...); + + if (!result.ptr) + { + aOomHandler(); + } + + return result; + } + + static void RiseOom(size_type aSize, alignment_type aAlignment) + { + auto pool = GetPool(); + + using func_t = void (*)(Memory::PoolStorage*, size_type, alignment_type); + RelocFunc func(Addresses::Memory_PoolStorage_OOM); + func(pool->storage, aSize, aAlignment); + } +}; +RED4EXT_ASSERT_SIZE(Allocator, 0x8); + +template +constexpr bool operator==(const Allocator&, const Allocator&) noexcept +{ + return std::is_same_v; +} +} // namespace RED4ext diff --git a/include/RED4ext/Memory/Pool.hpp b/include/RED4ext/Memory/Pool.hpp index 384a12c68..53c76860f 100644 --- a/include/RED4ext/Memory/Pool.hpp +++ b/include/RED4ext/Memory/Pool.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -21,7 +22,7 @@ struct PoolStorage uint32_t allocatorhandle; // 20 uint32_t allocatorId; // 24 - template + template T* GetAllocatorStorage() const { return reinterpret_cast(allocatorStorage & ~7); @@ -50,15 +51,17 @@ struct PoolRegistry SharedSpinLock nodesLock; // 00 PoolInfo nodes[MaxPoolCount]; // 08 - template + template + requires Detail::IsMemoryPool T* Get(const char* aName) { const auto id = FNV1a32(aName); return Get(id); } - template - T* Get(uint32_t aHandle) + template + requires Detail::IsMemoryPool + T* Get(std::uint32_t aHandle) { std::shared_lock _(nodesLock); @@ -66,7 +69,7 @@ struct PoolRegistry * be mantained, which is an overkill for this. Code that is using this should cache the value in a static * variable if the code is critical. */ - for (uint32_t i = 0; i < MaxPoolCount; i++) + for (std::uint32_t i = 0; i < MaxPoolCount; i++) { auto& node = nodes[i]; if (node.handle == aHandle)