/********************************************************************* * \file SlotMap.hpp * * \author Romain BOULLARD * \date May 2026 *********************************************************************/ #ifndef BIGFOOT_UTILS_CONTAINERS_SLOTMAP_HPP #define BIGFOOT_UTILS_CONTAINERS_SLOTMAP_HPP #include #include #include #include #include #include #include #include #include #include #include namespace Bigfoot { class SlotMapKey { public: using IndexType = std::uint32_t; using VersionType = std::uint32_t; private: static constexpr std::uint32_t VERSION_BIT_COUNT = sizeof(VersionType) * std::numeric_limits::digits; static constexpr std::uint32_t INDEX_BIT_COUNT = sizeof(IndexType) * std::numeric_limits::digits; static_assert(VERSION_BIT_COUNT + INDEX_BIT_COUNT <= 64, "We cant construct a 64 bit key from the given Version and Index types!"); static constexpr std::uint64_t INDEX_MASK = (static_cast(1) << INDEX_BIT_COUNT) - 1; public: static constexpr VersionType INVALID_VERSION = 0; static constexpr IndexType MAX_INDEX = std::numeric_limits::max(); constexpr SlotMapKey(const VersionType p_version, const IndexType p_index): m_key((static_cast(p_version) << INDEX_BIT_COUNT) | (static_cast(p_index) & INDEX_MASK)) { } constexpr SlotMapKey(): SlotMapKey(INVALID_VERSION, 0) { } constexpr SlotMapKey(const SlotMapKey& p_slotKey) = default; constexpr SlotMapKey(SlotMapKey&& p_slotKey) = default; constexpr ~SlotMapKey() = default; constexpr bool Valid() const { return GetVersion() != INVALID_VERSION; } constexpr VersionType GetVersion() const { return static_cast(m_key >> INDEX_BIT_COUNT); } constexpr IndexType GetIndex() const { return static_cast(m_key & INDEX_MASK); } constexpr SlotMapKey& operator=(const SlotMapKey& p_slotKey) = default; constexpr SlotMapKey& operator=(SlotMapKey&& p_slotKey) = default; [[nodiscard]] constexpr bool operator==(const SlotMapKey& p_other) const = default; private: std::uint64_t m_key; }; class SlotMapBase { public: ~SlotMapBase() = default; [[nodiscard]] bool Has(const SlotMapKey p_slotKey) const { return p_slotKey.Valid() && (p_slotKey.GetIndex() < m_slots.size() && p_slotKey.GetVersion() == m_slots[p_slotKey.GetIndex()].GetVersion()); } [[nodiscard]] std::size_t Size() const { return m_size; } [[nodiscard]] bool Empty() const { return m_size == 0; } SlotMapBase& operator=(const SlotMapBase& p_base) = default; SlotMapBase& operator=(SlotMapBase&& p_base) = default; protected: SlotMapBase() = default; SlotMapBase(const SlotMapBase& p_base) = default; SlotMapBase(SlotMapBase&& p_base) = default; [[nodiscard]] bool HasFreeSlots() const { return m_freeSlotHead != std::numeric_limits::max(); } void RecycleSlot(const SlotMapKey::VersionType p_version, const SlotMapKey::IndexType p_slotIndex) { if (p_version == SlotMapKey::INVALID_VERSION) { m_slots[p_slotIndex] = SlotMapKey {SlotMapKey::INVALID_VERSION, 0}; return; } m_slots[p_slotIndex] = SlotMapKey {p_version, m_freeSlotHead}; m_freeSlotHead = p_slotIndex; } void ResetSlots() { m_size = 0; m_slots.clear(); m_freeSlotHead = std::numeric_limits::max(); } void ReserveSlots(const std::size_t p_size) { m_slots.reserve(p_size); } std::size_t m_size = 0; eastl::vector m_slots; SlotMapKey::IndexType m_freeSlotHead = std::numeric_limits::max(); }; template class DenseSlotMap: public SlotMapBase { public: using iterator = typename eastl::vector::iterator; using const_iterator = typename eastl::vector::const_iterator; using reverse_iterator = typename eastl::vector::reverse_iterator; using const_reverse_iterator = typename eastl::vector::const_reverse_iterator; DenseSlotMap() = default; DenseSlotMap(const DenseSlotMap& p_map) = default; DenseSlotMap(DenseSlotMap&& p_map) = default; ~DenseSlotMap() = default; template [[nodiscard]] SlotMapKey Insert(ARGS&&... p_args) { const SlotMapKey::IndexType dataIndex = static_cast(m_size); m_data.emplace_back(std::forward(p_args)...); ++m_size; return AcquireSlot(dataIndex); } void Remove(const SlotMapKey p_slotKey) { if (!Has(p_slotKey)) { return; } const SlotMapKey::IndexType dataIndex = GetDataIndex(p_slotKey); if (dataIndex != m_size - 1) { m_data[dataIndex] = eastl::move(m_data.back()); } m_data.pop_back(); --m_size; ReleaseSlot(p_slotKey, dataIndex); } [[nodiscard]] TYPE* Get(const SlotMapKey p_slotKey) { return Has(p_slotKey) ? &m_data[GetDataIndex(p_slotKey)] : nullptr; } [[nodiscard]] const TYPE* Get(const SlotMapKey p_slotKey) const { return Has(p_slotKey) ? &m_data[GetDataIndex(p_slotKey)] : nullptr; } void Reset() { m_data.clear(); m_dataToSlots.clear(); ResetSlots(); } void Clear() { for (const std::size_t slotIndex: m_dataToSlots) { RecycleSlot(m_slots[slotIndex].GetVersion() + 1, static_cast(slotIndex)); } m_size = 0; m_data.clear(); m_dataToSlots.clear(); } [[nodiscard]] std::size_t Capacity() const { return m_data.capacity(); } void Reserve(const std::size_t p_size) { m_data.reserve(p_size); m_dataToSlots.reserve(p_size); ReserveSlots(p_size); } [[nodiscard]] iterator begin() { return m_data.begin(); } [[nodiscard]] iterator end() { return m_data.end(); } [[nodiscard]] const_iterator begin() const { return m_data.begin(); } [[nodiscard]] const_iterator end() const { return m_data.end(); } [[nodiscard]] reverse_iterator rbegin() { return m_data.rbegin(); } [[nodiscard]] reverse_iterator rend() { return m_data.rend(); } [[nodiscard]] const_reverse_iterator rbegin() const { return m_data.rbegin(); } [[nodiscard]] const_reverse_iterator rend() const { return m_data.rend(); } [[nodiscard]] const_iterator cbegin() const { return m_data.cbegin(); } [[nodiscard]] const_iterator cend() const { return m_data.cend(); } [[nodiscard]] const_reverse_iterator crbegin() const { return m_data.crbegin(); } [[nodiscard]] const_reverse_iterator crend() const { return m_data.crend(); } DenseSlotMap& operator=(const DenseSlotMap& p_map) = default; DenseSlotMap& operator=(DenseSlotMap&& p_map) = default; private: [[nodiscard]] SlotMapKey::IndexType GetDataIndex(const SlotMapKey p_slotKey) const { return m_slots[p_slotKey.GetIndex()].GetIndex(); } [[nodiscard]] SlotMapKey AcquireSlot(const SlotMapKey::IndexType p_dataIndex) { ASSERT(UtilsAssertHandler, m_slots.size() < SlotMapKey::MAX_INDEX, "All slots have been exhausted!"); if (HasFreeSlots()) { const SlotMapKey::IndexType slotIndex = m_freeSlotHead; m_freeSlotHead = m_slots[slotIndex].GetIndex(); m_slots[slotIndex] = SlotMapKey {m_slots[slotIndex].GetVersion(), p_dataIndex}; m_dataToSlots.push_back(slotIndex); return SlotMapKey {m_slots[slotIndex].GetVersion(), slotIndex}; } const SlotMapKey::IndexType slotIndex = static_cast(m_slots.size()); m_slots.emplace_back(1, p_dataIndex); m_dataToSlots.push_back(slotIndex); return SlotMapKey {1, slotIndex}; } void ReleaseSlot(const SlotMapKey p_slotKey, const SlotMapKey::IndexType p_dataIndex) { m_dataToSlots.erase_unsorted(m_dataToSlots.begin() + p_dataIndex); if (p_dataIndex < m_size) { m_slots[m_dataToSlots[p_dataIndex]] = SlotMapKey {m_slots[m_dataToSlots[p_dataIndex]].GetVersion(), p_dataIndex}; } RecycleSlot(p_slotKey.GetVersion() + 1, p_slotKey.GetIndex()); } eastl::vector m_data; eastl::vector m_dataToSlots; }; template class StableSlotMap: public SlotMapBase { private: static constexpr std::size_t PAGE_SIZE = 64; struct AlignedSlot { alignas(TYPE) std::byte m_bytes[sizeof(TYPE)]; }; struct Page { Page() = default; Page(const Page& p_page) = delete; Page(Page&& p_page) = delete; ~Page() { for (std::size_t offset = m_occupied.find_first(); offset != PAGE_SIZE; offset = m_occupied.find_next(offset)) { std::destroy_at(SlotAt(offset)); } } Page& operator=(const Page& p_page) = delete; Page& operator=(Page&& p_page) = delete; [[nodiscard]] TYPE* SlotAt(const std::size_t p_offset) { return std::launder(reinterpret_cast(m_storage[p_offset].m_bytes)); } [[nodiscard]] const TYPE* SlotAt(const std::size_t p_offset) const { return std::launder(reinterpret_cast(m_storage[p_offset].m_bytes)); } eastl::array m_storage; eastl::bitset m_occupied; }; public: class iterator { public: using iterator_category = eastl::bidirectional_iterator_tag; using value_type = TYPE; using difference_type = std::ptrdiff_t; using pointer = TYPE*; using reference = TYPE&; iterator() = default; [[nodiscard]] reference operator*() const { return *m_current; } [[nodiscard]] pointer operator->() const { return m_current; } iterator& operator++() { Advance(); return *this; } iterator operator++(int) { const iterator temp = *this; ++(*this); return temp; } iterator& operator--() { Retreat(); return *this; } iterator operator--(int) { const iterator temp = *this; --(*this); return temp; } [[nodiscard]] bool operator==(const iterator& p_other) const { return m_map == p_other.m_map && m_current == p_other.m_current; } private: friend class StableSlotMap; explicit iterator(StableSlotMap* p_map): m_map(p_map) { } void SeekFirstFrom(std::size_t p_pageIndex) { for (; p_pageIndex < m_map->m_pages.size(); ++p_pageIndex) { Page* const page = m_map->m_pages[p_pageIndex].get(); const std::size_t offset = page->m_occupied.find_first(); if (offset != PAGE_SIZE) { m_pageIndex = p_pageIndex; m_page = page; m_offset = offset; m_current = page->SlotAt(offset); return; } } m_pageIndex = m_map->m_pages.size(); m_page = nullptr; m_offset = 0; m_current = nullptr; } void SeekLastFrom(std::size_t p_pageIndex) { for (;;) { Page* const page = m_map->m_pages[p_pageIndex].get(); const std::size_t offset = page->m_occupied.find_last(); if (offset != PAGE_SIZE) { m_pageIndex = p_pageIndex; m_page = page; m_offset = offset; m_current = page->SlotAt(offset); return; } if (p_pageIndex == 0) { return; } --p_pageIndex; } } void Advance() { const std::size_t offset = m_page->m_occupied.find_next(m_offset); if (offset != PAGE_SIZE) { m_offset = offset; m_current = m_page->SlotAt(offset); return; } SeekFirstFrom(m_pageIndex + 1); } void Retreat() { if (m_current == nullptr) { SeekLastFrom(m_map->m_pages.size() - 1); return; } const std::size_t offset = m_page->m_occupied.find_prev(m_offset); if (offset != PAGE_SIZE) { m_offset = offset; m_current = m_page->SlotAt(offset); return; } if (m_pageIndex == 0) { return; } SeekLastFrom(m_pageIndex - 1); } StableSlotMap* m_map = nullptr; Page* m_page = nullptr; std::size_t m_pageIndex = 0; std::size_t m_offset = 0; TYPE* m_current = nullptr; }; class const_iterator { public: using iterator_category = eastl::bidirectional_iterator_tag; using value_type = TYPE; using difference_type = std::ptrdiff_t; using pointer = const TYPE*; using reference = const TYPE&; const_iterator() = default; [[nodiscard]] reference operator*() const { return *m_current; } [[nodiscard]] pointer operator->() const { return m_current; } const_iterator& operator++() { Advance(); return *this; } const_iterator operator++(int) { const const_iterator temp = *this; ++(*this); return temp; } const_iterator& operator--() { Retreat(); return *this; } const_iterator operator--(int) { const const_iterator temp = *this; --(*this); return temp; } [[nodiscard]] bool operator==(const const_iterator& p_other) const { return m_map == p_other.m_map && m_current == p_other.m_current; } private: friend class StableSlotMap; explicit const_iterator(const StableSlotMap* p_map): m_map(p_map) { } void SeekFirstFrom(std::size_t p_pageIndex) { for (; p_pageIndex < m_map->m_pages.size(); ++p_pageIndex) { const Page* const page = m_map->m_pages[p_pageIndex].get(); const std::size_t offset = page->m_occupied.find_first(); if (offset != PAGE_SIZE) { m_pageIndex = p_pageIndex; m_page = page; m_offset = offset; m_current = page->SlotAt(offset); return; } } m_pageIndex = m_map->m_pages.size(); m_page = nullptr; m_offset = 0; m_current = nullptr; } void SeekLastFrom(std::size_t p_pageIndex) { for (;;) { const Page* const page = m_map->m_pages[p_pageIndex].get(); const std::size_t offset = page->m_occupied.find_last(); if (offset != PAGE_SIZE) { m_pageIndex = p_pageIndex; m_page = page; m_offset = offset; m_current = page->SlotAt(offset); return; } if (p_pageIndex == 0) { return; } --p_pageIndex; } } void Advance() { const std::size_t offset = m_page->m_occupied.find_next(m_offset); if (offset != PAGE_SIZE) { m_offset = offset; m_current = m_page->SlotAt(offset); return; } SeekFirstFrom(m_pageIndex + 1); } void Retreat() { if (m_current == nullptr) { SeekLastFrom(m_map->m_pages.size() - 1); return; } const std::size_t offset = m_page->m_occupied.find_prev(m_offset); if (offset != PAGE_SIZE) { m_offset = offset; m_current = m_page->SlotAt(offset); return; } if (m_pageIndex == 0) { return; } SeekLastFrom(m_pageIndex - 1); } const StableSlotMap* m_map = nullptr; const Page* m_page = nullptr; std::size_t m_pageIndex = 0; std::size_t m_offset = 0; const TYPE* m_current = nullptr; }; using reverse_iterator = eastl::reverse_iterator; using const_reverse_iterator = eastl::reverse_iterator; StableSlotMap() = default; StableSlotMap(const StableSlotMap& p_map) = delete; StableSlotMap(StableSlotMap&& p_map) = default; ~StableSlotMap() = default; template [[nodiscard]] SlotMapKey Insert(ARGS&&... p_args) { const SlotMapKey key = AcquireFreeSlot(); const SlotMapKey::IndexType index = key.GetIndex(); const std::size_t pageIndex = index / PAGE_SIZE; if (pageIndex >= m_pages.size()) { m_pages.push_back(eastl::make_unique()); } Page& page = *m_pages[pageIndex]; const std::size_t offset = index % PAGE_SIZE; ::new (static_cast(page.m_storage[offset].m_bytes)) TYPE(std::forward(p_args)...); page.m_occupied.set(offset); ++m_size; return key; } void Remove(const SlotMapKey p_slotKey) { if (!Has(p_slotKey)) { return; } const SlotMapKey::IndexType index = p_slotKey.GetIndex(); Page& page = *m_pages[index / PAGE_SIZE]; const std::size_t offset = index % PAGE_SIZE; std::destroy_at(page.SlotAt(offset)); page.m_occupied.reset(offset); --m_size; ReleaseSlotVersion(index); } [[nodiscard]] TYPE* Get(const SlotMapKey p_slotKey) { return Has(p_slotKey) ? &DataAt(p_slotKey.GetIndex()) : nullptr; } [[nodiscard]] const TYPE* Get(const SlotMapKey p_slotKey) const { return Has(p_slotKey) ? &DataAt(p_slotKey.GetIndex()) : nullptr; } void Reset() { for (eastl::unique_ptr& page: m_pages) { page = eastl::make_unique(); } ResetSlots(); } void Clear() { for (std::size_t pageIndex = 0; pageIndex < m_pages.size(); ++pageIndex) { Page& page = *m_pages[pageIndex]; for (std::size_t offset = page.m_occupied.find_first(); offset != PAGE_SIZE; offset = page.m_occupied.find_next(offset)) { std::destroy_at(page.SlotAt(offset)); ReleaseSlotVersion(static_cast(pageIndex * PAGE_SIZE + offset)); } page.m_occupied.reset(); } m_size = 0; } [[nodiscard]] std::size_t Capacity() const { return m_pages.size() * PAGE_SIZE; } void Reserve(const std::size_t p_size) { const std::size_t pagesNeeded = (p_size + PAGE_SIZE - 1) / PAGE_SIZE; m_pages.reserve(pagesNeeded); for (std::size_t pageIndex = m_pages.size(); pageIndex < pagesNeeded; ++pageIndex) { m_pages.push_back(eastl::make_unique()); } ReserveSlots(p_size); } [[nodiscard]] iterator begin() { iterator it {this}; it.SeekFirstFrom(0); return it; } [[nodiscard]] iterator end() { return iterator {this}; } [[nodiscard]] const_iterator begin() const { const_iterator it {this}; it.SeekFirstFrom(0); return it; } [[nodiscard]] const_iterator end() const { return const_iterator {this}; } [[nodiscard]] reverse_iterator rbegin() { return reverse_iterator {end()}; } [[nodiscard]] reverse_iterator rend() { return reverse_iterator {begin()}; } [[nodiscard]] const_reverse_iterator rbegin() const { return const_reverse_iterator {end()}; } [[nodiscard]] const_reverse_iterator rend() const { return const_reverse_iterator {begin()}; } [[nodiscard]] const_iterator cbegin() const { return begin(); } [[nodiscard]] const_iterator cend() const { return end(); } [[nodiscard]] const_reverse_iterator crbegin() const { return rbegin(); } [[nodiscard]] const_reverse_iterator crend() const { return rend(); } StableSlotMap& operator=(const StableSlotMap& p_map) = delete; StableSlotMap& operator=(StableSlotMap&& p_map) = default; private: [[nodiscard]] TYPE& DataAt(const std::size_t p_index) { return *m_pages[p_index / PAGE_SIZE]->SlotAt(p_index % PAGE_SIZE); } [[nodiscard]] const TYPE& DataAt(const std::size_t p_index) const { return *m_pages[p_index / PAGE_SIZE]->SlotAt(p_index % PAGE_SIZE); } [[nodiscard]] SlotMapKey AcquireFreeSlot() { ASSERT(UtilsAssertHandler, m_slots.size() < SlotMapKey::MAX_INDEX, "All slots have been exhausted!"); if (HasFreeSlots()) { const SlotMapKey::IndexType slotIndex = m_freeSlotHead; m_freeSlotHead = m_slots[slotIndex].GetIndex(); m_slots[slotIndex] = SlotMapKey {m_slots[slotIndex].GetVersion(), slotIndex}; return m_slots[slotIndex]; } const SlotMapKey::IndexType slotIndex = static_cast(m_slots.size()); m_slots.emplace_back(1, slotIndex); return m_slots[slotIndex]; } void ReleaseSlotVersion(const SlotMapKey::IndexType p_index) { RecycleSlot(m_slots[p_index].GetVersion() + 1, p_index); } eastl::vector> m_pages; }; } // namespace Bigfoot #endif