Photon Editor Library 2.0.0-beta
A physically based renderer.
Loading...
Searching...
No Matches
TItemPool.h
Go to the documentation of this file.
1#pragma once
2
7
8#include <Common/assertion.h>
9#include <Common/primitive_type.h>
10#include <Common/memory.h>
11#include <Common/os.h>
12#include <Common/math_basics.h>
13#include <Common/exceptions.h>
14#include <Common/config.h>
15#include <Utility/utility.h>
16
17#include <cstddef>
18#include <vector>
19#include <utility>
20#include <memory>
21#include <iterator>
22#include <type_traits>
23#include <concepts>
24#include <new>
25#include <limits>
26#include <numeric>
27
28namespace ph::editor
29{
30
36template<
37 typename Item,
38 CHandleDispatcher Dispatcher = THandleDispatcher<TWeakHandle<Item>>,
39 typename ItemInterface = Item>
40class TItemPool : public TItemPoolInterface<ItemInterface, typename Dispatcher::HandleType>
41{
42 static_assert(std::is_move_constructible_v<Item>,
43 "Item must be move constructible.");
44
45public:
46 using HandleType = typename Dispatcher::HandleType;
47
48private:
50 using Index = typename HandleType::IndexType;
51 using Generation = typename HandleType::GenerationType;
52 using NonConstItem = std::remove_const_t<Item>;
53
54public:
59 template<typename ItemType> requires std::is_scalar_v<Item> || CBase<ItemType, Item>
61
62 static_assert(std::is_same_v<typename Base::HandleType, TCompatibleHandleType<Item>>,
63 "TItemPoolInterface is not using a compatible HandleType.");
64
65public:
66 template<bool IS_CONST>
68 {
69 public:
70 using ItemType = std::conditional_t<IS_CONST, const Item, Item>;
71 using PoolType = std::conditional_t<IS_CONST, const TItemPool, TItemPool>;
72
73 // Standard iterator traits
74 using iterator_category = std::bidirectional_iterator_tag;
76 using difference_type = std::ptrdiff_t;
77 using pointer = ItemType*;
79
80 // Default constructible
81 TIterator() = default;
82
83 TIterator(PoolType* const pool, Index currentIdx)
84 : m_pool(pool)
85 , m_currentIdx(currentIdx)
86 {}
87
88 // Dereferenceable
90 {
91 PH_ASSERT(m_pool);
92 PH_ASSERT_LT(m_currentIdx, m_pool->m_storageStates.size());
93 PH_ASSERT(!m_pool->m_storageStates[m_currentIdx].isFreed);
94
95 return *getItemPtr(m_pool->m_storageMemory, m_currentIdx);
96 }
97
98 // Pre-incrementable
100 {
101 PH_ASSERT(m_pool);
102
103 ++m_currentIdx;
104 m_currentIdx = m_pool->nextItemBeginIndex(m_currentIdx);
105 return *this;
106 }
107
108 // Post-incrementable
110 {
111 TIterator current = *this;
112 ++(*this);
113 return current;
114 }
115
116 // Pre-decrementable
118 {
119 PH_ASSERT(m_pool);
120
121 // Using current index as end index is equivalent to decrement by 1
122 m_currentIdx = m_pool->previousItemEndIndex(m_currentIdx);
123 PH_ASSERT_GT(m_currentIdx, 0);// If this method can be called, there must exist a previous
124 --m_currentIdx; // item and `m_currentIdx` is impossible to reach 0 since
125 return *this; // it is used as an (exclusive) end index here.
126 }
127
128 // Post-decrementable
130 {
131 TIterator current = *this;
132 --(*this);
133 return current;
134 }
135
136 // Equality
137 bool operator == (const TIterator& rhs) const
138 {
139 return m_currentIdx == rhs.m_currentIdx && m_pool == rhs.m_pool;
140 }
141
142 // Inequality
143 bool operator != (const TIterator& rhs) const
144 {
145 return !(*this == rhs);
146 }
147
152 {
153 PH_ASSERT(m_pool);
154 PH_ASSERT(!m_pool->m_storageStates[m_currentIdx].isFreed);
155
156 return m_pool->getHandleByIndex(m_currentIdx);
157 }
158
159 private:
160 PoolType* m_pool = nullptr;
161
162 // No hard referencing on the current item, so modification to the pool will not invalidate
163 // this iterator.
164 Index m_currentIdx = HandleType::INVALID_INDEX;
165 };
166
167public:
170
172 : m_storageMemory()
173 , m_storageStates()
174 , m_dispatcher()
175 , m_numItems(0)
176 {}
177
181 TItemPool(const TItemPool& other)
182 requires std::is_copy_constructible_v<Item> &&
183 std::is_copy_constructible_v<Dispatcher>
184
185 : m_storageMemory()
186 , m_storageStates()
187 , m_dispatcher(other.m_dispatcher)
188 , m_numItems(0)
189 {
190 grow(other.capacity());
191
192 other.forEachItemStorage(
193 [this, &other](const Item* otherItem, Index idx)
194 {
195 // Copy generation state, so handles from `other` will also work here
196 m_storageStates[idx].generation = other.m_storageStates[idx].generation;
197
198 if(!other.m_storageStates[idx].isFreed)
199 {
200 createItemAtIndex(idx, *otherItem);
201 }
202 });
203
204 PH_ASSERT_EQ(m_numItems, other.m_numItems);
205 }
206
207 TItemPool(TItemPool&& other) noexcept
208 : TItemPool()
209 {
210 swap(*this, other);
211 }
212
214 {
215 swap(*this, rhs);
216
217 return *this;
218 }
219
220 ~TItemPool() override
221 {
222 clear();
223
224 // All items should be removed at the end
225 PH_ASSERT_EQ(m_numItems, 0);
226 }
227
228 ItemInterface* accessItem(const HandleType& handle) override
229 {
230 return get(handle);
231 }
232
233 const ItemInterface* viewItem(const HandleType& handle) const override
234 {
235 return get(handle);
236 }
237
242 HandleType add(Item item)
243 {
244 return createAt(dispatchOneHandle(), std::move(item));
245 }
246
250 template<typename ItemType>
252 {
253 returnOneHandle(removeAt(handle));
254 }
255
261 template<typename ItemType>
263 {
264 constexpr auto initialGeneration = HandleType::nextGeneration(HandleType::INVALID_GENERATION);
265
266 const bool isEmpty = handle.isEmpty();
267 const bool isInvalidOutOfBound = handle.getIndex() >= capacity() && handle.getGeneration() != initialGeneration;
268 const bool isStale = handle.getIndex() < capacity() && !isFresh(handle);
269 if(isEmpty || isInvalidOutOfBound || isStale)
270 {
271 throw_formatted<IllegalOperationException>(
272 "creating item with bad handle {}", handle);
273 }
274
275 // Potentially create new storage space
276 const Index itemIdx = handle.getIndex();
277 if(!hasFreeSpace())
278 {
279 if(capacity() == maxCapacity())
280 {
281 throw_formatted<OverflowException>(
282 "Storage size will exceed the maximum amount Index type can hold (max={})",
283 maxCapacity());
284 }
285
286 PH_ASSERT_LT(itemIdx, maxCapacity());
287 const Index newCapacity = std::max(nextCapacity(capacity()), itemIdx + 1);
288 grow(newCapacity);
289 }
290
291 // At this point, storage size must have been grown to cover `itemIdx`
292 PH_ASSERT_LT(itemIdx, m_storageStates.size());
293 PH_ASSERT(isFresh(handle));
294
295 if(!m_storageStates[itemIdx].isFreed)
296 {
297 throw_formatted<IllegalOperationException>(
298 "attempting to create item at an occupied slot (handle: {})", handle);
299 }
300
301 createItemAtIndex(itemIdx, std::move(item));
302 return getHandleByIndex(itemIdx);
303 }
304
311 template<typename ItemType>
312 [[nodiscard]]
314 {
315 if(!isFresh(handle))
316 {
317 throw_formatted<IllegalOperationException>(
318 "removing item with stale handle {}", handle);
319 }
320
321 const Index itemIdx = handle.getIndex();
322
323 // Generally should not happen: the generation counter increases on each item removal, using
324 // the same handle to call this method again will result in `isFresh()` being false which
325 // will throw. If this fails, could be generation collision, use after removal, remove before
326 // creation, or bad handles (from wrong pool).
327 if(m_storageStates[itemIdx].isFreed)
328 {
329 throw_formatted<IllegalOperationException>(
330 "attempting to remove item at an emptied slot (handle: {})", handle);
331 }
332
333 removeItemAtIndex(itemIdx);
334 return getHandleByIndex(itemIdx);
335 }
336
340 [[nodiscard]]
342 {
343 // Note: call the dispatcher without touching internal states, as this method may be called
344 // with a different policy (e.g., from a different thread, depending on the dispatcher used)
345 return m_dispatcher.dispatchOne();
346 }
347
351 template<typename ItemType>
353 {
354 HandleType nativeHandle(handle.getIndex(), handle.getGeneration());
355
356 // Note: call the dispatcher without touching internal states, as this method may be called
357 // with a different policy (e.g., from a different thread, depending on the dispatcher used)
358 m_dispatcher.returnOne(nativeHandle);
359 }
360
361 void clear()
362 {
363 if(isEmpty())
364 {
365 return;
366 }
367
368 forEachItem(
369 [this](Item* /* item */, Index idx)
370 {
371 removeItemAtIndex(idx);
372 returnOneHandle(getHandleByIndex(idx));
373 });
374 }
375
381 template<typename ItemType>
382 ItemType* get(const TCompatibleHandleType<ItemType>& handle)
383 {
384 return isFresh(handle) && !m_storageStates[handle.getIndex()].isFreed
385 ? getItemPtr(m_storageMemory, handle.getIndex())
386 : nullptr;
387 }
388
394 template<typename ItemType>
395 const ItemType* get(const TCompatibleHandleType<ItemType>& handle) const
396 {
397 return isFresh(handle) && !m_storageStates[handle.getIndex()].isFreed
398 ? getItemPtr(m_storageMemory, handle.getIndex())
399 : nullptr;
400 }
401
402 template<typename ItemType>
405 {
407 using EmbeddedWeakHandle = typename StrongHandle::WeakHandleType;
408
409 static_assert(std::convertible_to<decltype(handle), EmbeddedWeakHandle>,
410 "Input handle type cannot be converted to a strong handle.");
411
412 return StrongHandle(EmbeddedWeakHandle(handle), this);
413 }
414
415 Index numItems() const
416 {
417 PH_ASSERT_LE(m_numItems, capacity());
418 return m_numItems;
419 }
420
421 Index numFreeSpace() const
422 {
423 PH_ASSERT_LE(m_numItems, capacity());
424 return capacity() - m_numItems;
425 }
426
427 Index capacity() const
428 {
429 PH_ASSERT_LE(m_storageStates.size(), maxCapacity());
430 return static_cast<Index>(m_storageStates.size());
431 }
432
436 bool isEmpty() const
437 {
438 return numItems() == 0;
439 }
440
441 bool hasFreeSpace() const
442 {
443 return numFreeSpace() > 0;
444 }
445
450 template<typename ItemType>
451 bool isFresh(const TCompatibleHandleType<ItemType>& handle) const
452 {
453 return handle.getIndex() < m_storageStates.size() &&
454 handle.getGeneration() == m_storageStates[handle.getIndex()].generation;
455 }
456
461 {
462 return IteratorType(this, nextItemBeginIndex(0));
463 }
464
466 {
467 return IteratorType(this, capacity());
468 }
469
471 {
472 return IteratorType(this, nextItemBeginIndex(0));
473 }
474
476 {
477 return IteratorType(this, capacity());
478 }
480
481 static constexpr Index maxCapacity()
482 {
483 return std::numeric_limits<Index>::max();
484 }
485
486 friend void swap(TItemPool& first, TItemPool& second) noexcept
487 {
488 // Enable ADL
489 using std::swap;
490
491 swap(first.m_storageMemory, second.m_storageMemory);
492 swap(first.m_storageStates, second.m_storageStates);
493 swap(first.m_dispatcher, second.m_dispatcher);
494 swap(first.m_numItems, second.m_numItems);
495 }
496
497private:
498 void createItemAtIndex(const Index itemIdx, Item item)
499 {
500 PH_ASSERT_LT(itemIdx, m_storageStates.size());
501 PH_ASSERT(m_storageStates[itemIdx].isFreed);
502
503 // `Item` was manually destroyed. No need for storing the returned pointer nor using
504 // `std::launder()` on each use for most cases (same object type with exactly the same storage
505 // location), see C++ standard [basic.life] section 8 (https://timsong-cpp.github.io/cppwp/basic.life#8).
506 std::construct_at(getItemPtrDirectly(m_storageMemory, itemIdx), std::move(item));
507
508 m_storageStates[itemIdx].isFreed = false;
509 ++m_numItems;
510 }
511
512 void removeItemAtIndex(const Index itemIdx)
513 {
514 PH_ASSERT_LT(itemIdx, capacity());
515 PH_ASSERT(!m_storageStates[itemIdx].isFreed);
516
517 std::destroy_at(getItemPtr(m_storageMemory, itemIdx));
518
519 m_storageStates[itemIdx].isFreed = true;
520 m_storageStates[itemIdx].generation = HandleType::nextGeneration(m_storageStates[itemIdx].generation);
521 --m_numItems;
522 }
523
524 HandleType getHandleByIndex(const Index itemIdx) const
525 {
526 return HandleType(itemIdx, m_storageStates[itemIdx].generation);
527 }
528
529 void grow(const Index newCapacity)
530 {
531 const Index oldCapacity = capacity();
532 PH_ASSERT_GT(newCapacity, oldCapacity);
533
534 const auto requiredMemorySize = newCapacity * sizeof(Item);
535 const auto alignmentSize = std::lcm(alignof(Item), os::get_L1_cache_line_size_in_bytes());
536 const auto totalMemorySize = math::next_multiple(requiredMemorySize, alignmentSize);
537
538 // Create new item storage and move items over
539
540 TAlignedMemoryUniquePtr<NonConstItem> newStorageMemory =
541 make_aligned_memory<NonConstItem>(totalMemorySize, alignmentSize);
542 if(!newStorageMemory)
543 {
544 throw std::bad_alloc{};
545 }
546
547 forEachItem(
548 [&newStorageMemory](Item* oldItem, Index idx)
549 {
550 std::construct_at(getItemPtrDirectly(newStorageMemory, idx), std::move(*oldItem));
551 std::destroy_at(oldItem);
552 });
553
554 // Extend storage states to cover new storage spaces
555 m_storageStates.resize(newCapacity, StorageState{});
556
557 // Finally, get rid of the old item storage
558 m_storageMemory = std::move(newStorageMemory);
559 }
560
561 template<typename PerItemStorageOp>
562 void forEachItemStorage(PerItemStorageOp op)
563 {
564 for(Index itemIdx = 0; itemIdx < capacity(); ++itemIdx)
565 {
566 Item* item = m_storageStates[itemIdx].isFreed
567 ? getItemPtrDirectly(m_storageMemory, itemIdx)
568 : getItemPtr(m_storageMemory, itemIdx);
569 op(item, itemIdx);
570 }
571 }
572
573 template<typename PerItemStorageOp>
574 void forEachItemStorage(PerItemStorageOp op) const
575 {
576 for(Index itemIdx = 0; itemIdx < capacity(); ++itemIdx)
577 {
578 const Item* item = m_storageStates[itemIdx].isFreed
579 ? getItemPtrDirectly(m_storageMemory, itemIdx)
580 : getItemPtr(m_storageMemory, itemIdx);
581 op(item, itemIdx);
582 }
583 }
584
585 template<typename PerItemOp>
586 void forEachItem(PerItemOp op)
587 {
588 forEachItemStorage(
589 [op, this](Item* item, Index idx)
590 {
591 if(m_storageStates[idx].isFreed)
592 {
593 return;
594 }
595
596 op(item, idx);
597 });
598 }
599
600 template<typename PerItemOp>
601 void forEachItem(PerItemOp op) const
602 {
603 forEachItemStorage(
604 [op, this](const Item* item, Index idx)
605 {
606 if(m_storageStates[idx].isFreed)
607 {
608 return;
609 }
610
611 op(item, idx);
612 });
613 }
614
615 Index nextItemBeginIndex(const Index beginIdx) const
616 {
617 // If failed, most likely be using an out-of-range iterator
618 PH_ASSERT_IN_RANGE_INCLUSIVE(beginIdx, 0, capacity());
619
620 Index itemBeginIdx = beginIdx;
621 while(itemBeginIdx < capacity())
622 {
623 if(m_storageStates[itemBeginIdx].isFreed)
624 {
625 ++itemBeginIdx;
626 }
627 else
628 {
629 break;
630 }
631 }
632 return itemBeginIdx;
633 }
634
635 Index previousItemEndIndex(const Index endIdx) const
636 {
637 // If failed, most likely be using an out-of-range iterator
638 PH_ASSERT_IN_RANGE_INCLUSIVE(endIdx, 0, capacity());
639
640 Index itemEndIdx = endIdx;
641 while(itemEndIdx > 0)
642 {
643 if(m_storageStates[itemEndIdx - 1].isFreed)
644 {
645 --itemEndIdx;
646 }
647 else
648 {
649 break;
650 }
651 }
652 return itemEndIdx;
653 }
654
659 static auto getItemPtr(
660 const TAlignedMemoryUniquePtr<NonConstItem>& storage,
661 const std::size_t index)
662 -> NonConstItem*
663 {
664 // If `Item` is const qualified, laundering is required to prevent aggressive constant folding.
665 // See [basic.life] section 8.3 (https://timsong-cpp.github.io/cppwp/basic.life#8.3)
666 // vvv currently not required as we are storing `NonConstItem`
667 if constexpr(/* std::is_const_v<Item> || */ PH_STRICT_OBJECT_LIFETIME)
668 {
669 // UB if pointed-to object not within its lifetime
670 return std::launder(getItemPtrDirectly(storage, index));
671 }
672 // We do not need to launder even if `Item` contains const or reference members. See
673 // https://stackoverflow.com/questions/62642542/were-all-implementations-of-stdvector-non-portable-before-stdlaunder
674 else
675 {
676 return getItemPtrDirectly(storage, index);
677 }
678 }
679
680 static auto getItemPtrDirectly(
681 const TAlignedMemoryUniquePtr<NonConstItem>& storage,
682 const std::size_t index)
683 -> NonConstItem*
684 {
685 return storage.get() + index;
686 }
687
688 static constexpr Index nextCapacity(const Index currentCapacity)
689 {
690 // Effective growth rate k = 1.5
691 const Index oldCapacity = currentCapacity;
692 const Index additionalCapacity = oldCapacity / 2 + 1;
693 const Index newCapacity = (maxCapacity() - oldCapacity >= additionalCapacity)
694 ? oldCapacity + additionalCapacity : maxCapacity();
695 return newCapacity;
696 }
697
698private:
699 struct StorageState
700 {
701 Generation generation = HandleType::nextGeneration(HandleType::INVALID_GENERATION);
702 uint8 isFreed : 1 = true;
703
704 // Can support more tags, e.g., isDisabled for disabling an item slot permanently
705 // so generation collision will never happen.
706 };
707
708 // LWG 3870 forbids `std::construct_at` to modify/create const objects. We store all items
709 // as `NonConstItem` and rely on implicit casts to const if required. This seems to be a weaker
710 // part of the standard and supporting const storage does not worth the risk of UB for now.
711 // See `getItemPtr()` for more important things to know for supporting const items.
712 TAlignedMemoryUniquePtr<NonConstItem> m_storageMemory;
713
714 std::vector<StorageState> m_storageStates;
715 Dispatcher m_dispatcher;
716 Index m_numItems;
717};
718
719}// end namespace ph::editor
Definition TItemPool.h:68
reference operator*() const
Definition TItemPool.h:89
std::bidirectional_iterator_tag iterator_category
Definition TItemPool.h:74
bool operator!=(const TIterator &rhs) const
Definition TItemPool.h:143
HandleType getHandle() const
Definition TItemPool.h:151
bool operator==(const TIterator &rhs) const
Definition TItemPool.h:137
ItemType * pointer
Definition TItemPool.h:77
ItemType value_type
Definition TItemPool.h:75
TIterator(PoolType *const pool, Index currentIdx)
Definition TItemPool.h:83
std::conditional_t< IS_CONST, const TItemPool, TItemPool > PoolType
Definition TItemPool.h:71
TIterator & operator++()
Definition TItemPool.h:99
TIterator & operator--()
Definition TItemPool.h:117
ItemType & reference
Definition TItemPool.h:78
std::ptrdiff_t difference_type
Definition TItemPool.h:76
std::conditional_t< IS_CONST, const Item, Item > ItemType
Definition TItemPool.h:70
A general item pool.
Definition TItemPool.h:41
~TItemPool() override
Definition TItemPool.h:220
friend void swap(TItemPool &first, TItemPool &second) noexcept
Definition TItemPool.h:486
Index capacity() const
Definition TItemPool.h:427
HandleType add(Item item)
Definition TItemPool.h:242
Index numFreeSpace() const
Definition TItemPool.h:421
const ItemInterface * viewItem(const HandleType &handle) const override
Definition TItemPool.h:233
ConstIteratorType cend()
Definition TItemPool.h:475
HandleType createAt(const TCompatibleHandleType< ItemType > &handle, Item item)
Place item at the storage slot indicated by handle. Manual handle management API. Will not invalidate...
Definition TItemPool.h:262
bool isEmpty() const
Definition TItemPool.h:436
IteratorType end()
Definition TItemPool.h:465
const ItemType * get(const TCompatibleHandleType< ItemType > &handle) const
Get item by handle. Complexity: O(1).
Definition TItemPool.h:395
TIterator< false > IteratorType
Definition TItemPool.h:168
void returnOneHandle(const TCompatibleHandleType< ItemType > &handle)
Definition TItemPool.h:352
ItemType * get(const TCompatibleHandleType< ItemType > &handle)
Get item by handle. Complexity: O(1).
Definition TItemPool.h:382
static constexpr Index maxCapacity()
Definition TItemPool.h:481
TItemPool()
Definition TItemPool.h:171
auto getStrong(const TCompatibleHandleType< ItemType > &handle) -> TStrongHandle< ItemInterface, Index, Generation >
Definition TItemPool.h:403
TItemPool(TItemPool &&other) noexcept
Definition TItemPool.h:207
typename Dispatcher::HandleType HandleType
Definition TItemPool.h:46
IteratorType begin()
Iterators for all created items in the pool.
Definition TItemPool.h:460
HandleType removeAt(const TCompatibleHandleType< ItemType > &handle)
Remove the item at the storage slot indicated by handle. Manual handle management API....
Definition TItemPool.h:313
bool isFresh(const TCompatibleHandleType< ItemType > &handle) const
Definition TItemPool.h:451
void clear()
Definition TItemPool.h:361
bool hasFreeSpace() const
Definition TItemPool.h:441
TItemPool(const TItemPool &other)
Copy items stored in other into this pool. Handles that were originally valid for other will also be ...
Definition TItemPool.h:181
ItemInterface * accessItem(const HandleType &handle) override
Definition TItemPool.h:228
ConstIteratorType cbegin()
Definition TItemPool.h:470
TItemPool & operator=(TItemPool rhs) noexcept
Definition TItemPool.h:213
void remove(const TCompatibleHandleType< ItemType > &handle)
Remove the item at the storage slot indicated by handle. Will not invalidate iterators....
Definition TItemPool.h:251
Index numItems() const
Definition TItemPool.h:415
HandleType dispatchOneHandle()
Definition TItemPool.h:341
Definition TItemPoolInterface.h:10
Handle with strong reference semantics. Default constructor creates empty handle.
Definition TStrongHandle.h:18
Handle with weak reference semantics. Default constructor creates empty handle.
Definition TWeakHandle.h:23
Index getIndex() const
Definition TWeakHandle.h:49
Generation getGeneration() const
Definition TWeakHandle.h:54
bool isEmpty() const
Definition TWeakHandle.h:62
Definition ph_editor.h:10