Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
consumed_registry.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <concepts>
8#include <cstddef>
9#include <functional>
10#include <memory>
11#include <memory_resource>
12#include <ranges>
13#include <span>
14#include <utility>
15#include <vector>
16
17#ifdef HELIOS_STL_FLAT_MAP_AVAILABLE
18#include <flat_map>
19#else
20#include <boost/container/flat_map.hpp>
21#endif
22
23namespace helios::ecs {
24
25/**
26 * @brief Per-system registry for tracking consumed message indices.
27 * @details Each system running in parallel gets its own
28 * `ConsumedMessagesRegistry` instance. Systems mark messages as consumed by
29 * writing indices into their registry, avoiding data races. At Update time, all
30 * registries are merged and applied to message queues in `MessageManager`.
31 *
32 * Indices are global within the combined (previous + current) message view for
33 * a given type. For a type with P previous messages and C current messages:
34 * - Indices [0, P) refer to previous messages
35 * - Indices [P, P + C) refer to current messages
36 *
37 * @note Not therad-safe.
38 * @tparam Alloc Allocator type for internal storage (default:
39 * `std::allocator<std::byte>`)
40 */
41template <typename Alloc = std::allocator<std::byte>>
43public:
44 using size_type = size_t;
45 using allocator_type = Alloc;
46
47private:
48 using IndicesAllocator = typename std::allocator_traits<
49 allocator_type>::template rebind_alloc<size_type>;
50 using ConsumedIndices = std::vector<size_type, IndicesAllocator>;
51
52#ifdef HELIOS_STL_FLAT_MAP_AVAILABLE
53 using KeyContainer =
54 std::vector<MessageTypeIndex,
55 typename std::allocator_traits<
56 allocator_type>::template rebind_alloc<MessageTypeIndex>>;
57 using MappedContainer =
58 std::vector<ConsumedIndices,
59 typename std::allocator_traits<
60 allocator_type>::template rebind_alloc<ConsumedIndices>>;
61
62 using ConsumedMap =
63 std::flat_map<MessageTypeIndex, ConsumedIndices,
64 std::less<MessageTypeIndex>, KeyContainer, MappedContainer>;
65#else
66 using MapValueType = std::pair<MessageTypeIndex, ConsumedIndices>;
67 using MapAllocator = typename std::allocator_traits<
68 allocator_type>::template rebind_alloc<MapValueType>;
69
70 using ConsumedMap =
71 boost::container::flat_map<MessageTypeIndex, ConsumedIndices,
72 std::less<MessageTypeIndex>, MapAllocator>;
73#endif
74
75#ifdef HELIOS_STL_FLAT_MAP_AVAILABLE
76public:
77 explicit constexpr ConsumedMessagesRegistry(allocator_type alloc = {})
78 : consumed_(alloc), allocator_(std::move(alloc)) {}
79#else
80public:
81 explicit constexpr ConsumedMessagesRegistry(allocator_type alloc = {})
82 : consumed_(MapAllocator(alloc)), allocator_(std::move(alloc)) {}
83#endif
84
85 /**
86 * @brief Constructs a `ConsumedMessagesRegistry` from a PMR memory resource.
87 * @details Enabled only when `allocator_type` is constructible from
88 * `std::pmr::memory_resource*`.
89 * @param resource Memory resource used to construct allocator
90 */
91 explicit constexpr ConsumedMessagesRegistry(
92 std::pmr::memory_resource* resource)
93 requires std::constructible_from<allocator_type, std::pmr::memory_resource*>
95
96 ConsumedMessagesRegistry(std::nullptr_t) = delete;
97
101
103 default;
105 default;
106
107 /**
108 * @brief Marks an message as consumed by its type and global index.
109 * @tparam T Consumable message type
110 * @param global_index Index of the message in the combined (previous +
111 * current) view
112 */
113 template <ConsumableMessageTrait T>
114 constexpr void MarkConsumed(size_type global_index) {
116 }
117
118 /**
119 * @brief Marks an message as consumed by its type index and global index.
120 * @param type_index Type index of the message
121 * @param global_index Index of the message in the combined (previous +
122 * current) view
123 */
124 constexpr void MarkConsumed(MessageTypeIndex type_index,
125 size_type global_index);
126
127 /**
128 * @brief Merges consumed entries from another registry into this one.
129 * @details Performs a sorted union of consumed indices for each type.
130 * Avoids additional heap allocation by appending and using in-place merge.
131 * @tparam OtherAlloc Allocator type of the source registry
132 * @param other Registry to merge from
133 */
134 template <typename OtherAlloc>
136
137 /**
138 * @brief Merges consumed entries from another registry into this one.
139 * @details Rvalue overload that consumes the source registry.
140 * @tparam OtherAlloc Allocator type of the source registry
141 * @param other Registry to merge from
142 */
143 template <typename OtherAlloc>
145
146 /// @brief Clears all consumed entries.
147 constexpr void Clear() noexcept { consumed_.clear(); }
148
149 /**
150 * @brief Clears consumed entries for a specific message type.
151 * @tparam T Consumable message type
152 */
153 template <ConsumableMessageTrait T>
154 constexpr void Clear() noexcept {
156 }
157
158 /**
159 * @brief Clears consumed entries for a specific message type.
160 * @param type_index Type index to clear
161 */
162 constexpr void Clear(MessageTypeIndex type_index) noexcept {
163 consumed_.erase(type_index);
164 }
165
166 /**
167 * @brief Checks if a specific message is marked as consumed.
168 * @tparam T Consumable message type
169 * @param global_index Global index of the message
170 * @return True if the message is consumed, false otherwise
171 */
172 template <ConsumableMessageTrait T>
173 [[nodiscard]] constexpr bool IsConsumed(
174 size_type global_index) const noexcept {
175 return IsConsumed(MessageTypeIndex::From<T>(), global_index);
176 }
177
178 /**
179 * @brief Checks if a specific message is marked as consumed.
180 * @param type_index Type index of the message
181 * @param global_index Global index of the message
182 * @return True if the message is consumed, false otherwise
183 */
184 [[nodiscard]] constexpr bool IsConsumed(
185 MessageTypeIndex type_index, size_type global_index) const noexcept;
186
187 /**
188 * @brief Gets the sorted consumed indices for a specific message type.
189 * @tparam T Consumable message type
190 * @return Span of sorted consumed indices, or empty span if none
191 */
192 template <ConsumableMessageTrait T>
193 [[nodiscard]] constexpr auto ConsumedIndicesFor() const noexcept
194 -> std::span<const size_type> {
196 }
197
198 /**
199 * @brief Gets the sorted consumed indices for a specific message type.
200 * @param type_index Type index of the message
201 * @return Span of sorted consumed indices, or empty span if none
202 */
203 [[nodiscard]] constexpr auto ConsumedIndicesFor(
204 MessageTypeIndex type_index) const noexcept -> std::span<const size_type>;
205
206 /**
207 * @brief Checks if any message type has consumed entries.
208 * @return True if no consumed entries exist, false otherwise
209 */
210 [[nodiscard]] constexpr bool Empty() const noexcept {
211 return consumed_.empty();
212 }
213
214 /**
215 * @brief Checks if a specific message type has consumed entries.
216 * @tparam T Consumable message type
217 * @return True if consumed entries exist for this type, false otherwise
218 */
219 template <ConsumableMessageTrait T>
220 [[nodiscard]] constexpr bool HasConsumed() const noexcept {
222 }
223
224 /**
225 * @brief Checks if a specific message type has consumed entries.
226 * @param type_index Type index to check
227 * @return True if consumed entries exist for this type, false otherwise
228 */
229 [[nodiscard]] constexpr bool HasConsumed(
230 MessageTypeIndex type_index) const noexcept;
231
232 /**
233 * @brief Gets the total number of consumed messages across all types.
234 * @return Total consumed message count
235 */
236 [[nodiscard]] constexpr size_type TotalConsumedCount() const noexcept;
237
238 /**
239 * @brief Gets the number of consumed messages for a specific type.
240 * @tparam T Consumable message type
241 * @return Number of consumed messages for this type
242 */
243 template <ConsumableMessageTrait T>
244 [[nodiscard]] constexpr size_type ConsumedCount() const noexcept {
246 }
247
248 /**
249 * @brief Gets the number of consumed messages for a specific type.
250 * @param type_index Type index to query
251 * @return Number of consumed messages for this type
252 */
253 [[nodiscard]] constexpr size_type ConsumedCount(
254 MessageTypeIndex type_index) const noexcept;
255
256 /**
257 * @brief Provides direct access to the underlying consumed map.
258 * @return Const reference to the consumed map
259 */
260 [[nodiscard]] constexpr const ConsumedMap& Data() const noexcept {
261 return consumed_;
262 }
263
264private:
265 [[nodiscard]] constexpr auto EnsureIndices(MessageTypeIndex type_index)
266 -> ConsumedIndices&;
267
268 ConsumedMap consumed_; ///< Map of message type index to sorted consumed
269 ///< global indices
270 [[no_unique_address]] allocator_type allocator_;
271};
272
273template <typename Alloc>
274constexpr auto ConsumedMessagesRegistry<Alloc>::EnsureIndices(
275 MessageTypeIndex type_index) -> ConsumedIndices& {
276 const auto it = consumed_.lower_bound(type_index);
277 if (it != consumed_.end() && it->first == type_index) {
278 return it->second;
279 }
280
281 return consumed_
282 .emplace_hint(it, type_index,
283 ConsumedIndices{IndicesAllocator{allocator_}})
284 ->second;
285}
286
287template <typename Alloc>
289 MessageTypeIndex type_index, size_type global_index) {
290 auto& indices = EnsureIndices(type_index);
291 // Insert in sorted order, maintaining uniqueness
292 const auto pos = std::ranges::lower_bound(indices, global_index);
293 if (pos == indices.end() || *pos != global_index) {
294 indices.insert(pos, global_index);
295 }
296}
297
298template <typename Alloc>
299template <typename OtherAlloc>
302 for (const auto& [type_index, other_indices] : other.Data()) {
303 if (other_indices.empty()) {
304 continue;
305 }
306
307 auto& our_indices = EnsureIndices(type_index);
308 if (our_indices.empty()) {
309 our_indices.insert(our_indices.end(), other_indices.begin(),
310 other_indices.end());
311 continue;
312 }
313
314 // In-place merge: append other indices, then inplace_merge, then
315 // deduplicate. This reuses existing capacity in our_indices, avoiding a
316 // separate allocation.
317 const auto original_size = our_indices.size();
318 our_indices.insert(our_indices.end(), other_indices.begin(),
319 other_indices.end());
320 std::ranges::inplace_merge(
321 our_indices,
322 our_indices.begin() + static_cast<ptrdiff_t>(original_size));
323 const auto [first, last] = std::ranges::unique(our_indices);
324 our_indices.erase(first, last);
325 }
326}
327
328template <typename Alloc>
329template <typename OtherAlloc>
332 for (auto&& [type_index, other_indices] : other.consumed_) {
333 if (other_indices.empty()) {
334 continue;
335 }
336
337 auto& our_indices = EnsureIndices(type_index);
338 if (our_indices.empty()) {
339 our_indices = std::move(other_indices);
340 continue;
341 }
342
343 const auto original_size = our_indices.size();
344 our_indices.insert(our_indices.end(),
345 std::make_move_iterator(other_indices.begin()),
346 std::make_move_iterator(other_indices.end()));
347 std::ranges::inplace_merge(
348 our_indices,
349 our_indices.begin() + static_cast<ptrdiff_t>(original_size));
350 const auto [first, last] = std::ranges::unique(our_indices);
351 our_indices.erase(first, last);
352 }
353
354 other.consumed_.clear();
355}
356
357template <typename Alloc>
359 MessageTypeIndex type_index, size_type global_index) const noexcept {
360 const auto it = consumed_.find(type_index);
361 if (it == consumed_.end()) {
362 return false;
363 }
364 return std::ranges::binary_search(it->second, global_index);
365}
366
367template <typename Alloc>
369 MessageTypeIndex type_index) const noexcept -> std::span<const size_type> {
370 const auto it = consumed_.find(type_index);
371 if (it == consumed_.end()) {
372 return {};
373 }
374 return {it->second.data(), it->second.size()};
375}
376
377template <typename Alloc>
379 MessageTypeIndex type_index) const noexcept {
380 const auto it = consumed_.find(type_index);
381 return it != consumed_.end() && !it->second.empty();
382}
383
384template <typename Alloc>
386 const noexcept -> size_type {
387 size_type total = 0;
388 for (const auto& [_, indices] : consumed_) {
389 total += indices.size();
390 }
391 return total;
392}
393
394template <typename Alloc>
396 MessageTypeIndex type_index) const noexcept -> size_type {
397 const auto it = consumed_.find(type_index);
398 if (it == consumed_.end()) {
399 return 0;
400 }
401 return it->second.size();
402}
403
405 std::pmr::polymorphic_allocator<std::byte>>;
406
407} // namespace helios::ecs
Per-system registry for tracking consumed message indices.
constexpr bool HasConsumed(MessageTypeIndex type_index) const noexcept
Checks if a specific message type has consumed entries.
constexpr auto ConsumedIndicesFor() const noexcept -> std::span< const size_type >
Gets the sorted consumed indices for a specific message type.
constexpr size_type ConsumedCount(MessageTypeIndex type_index) const noexcept
Gets the number of consumed messages for a specific type.
ConsumedMessagesRegistry(ConsumedMessagesRegistry &&) noexcept=default
constexpr void MergeFrom(const ConsumedMessagesRegistry< OtherAlloc > &other)
Merges consumed entries from another registry into this one.
constexpr ConsumedMessagesRegistry(std::pmr::memory_resource *resource)
Constructs a ConsumedMessagesRegistry from a PMR memory resource.
constexpr void MarkConsumed(MessageTypeIndex type_index, size_type global_index)
Marks an message as consumed by its type index and global index.
constexpr ConsumedMessagesRegistry(allocator_type alloc={})
constexpr void Clear() noexcept
Clears all consumed entries.
constexpr auto ConsumedIndicesFor(MessageTypeIndex type_index) const noexcept -> std::span< const size_type >
Gets the sorted consumed indices for a specific message type.
constexpr void Clear(MessageTypeIndex type_index) noexcept
Clears consumed entries for a specific message type.
constexpr const ConsumedMap & Data() const noexcept
Provides direct access to the underlying consumed map.
constexpr void Clear() noexcept
Clears consumed entries for a specific message type.
constexpr bool HasConsumed() const noexcept
Checks if a specific message type has consumed entries.
constexpr bool Empty() const noexcept
Checks if any message type has consumed entries.
constexpr size_type TotalConsumedCount() const noexcept
Gets the total number of consumed messages across all types.
constexpr bool IsConsumed(MessageTypeIndex type_index, size_type global_index) const noexcept
Checks if a specific message is marked as consumed.
ConsumedMessagesRegistry(std::nullptr_t)=delete
constexpr void MergeFrom(ConsumedMessagesRegistry< OtherAlloc > &&other)
Merges consumed entries from another registry into this one.
ConsumedMessagesRegistry(const ConsumedMessagesRegistry &)=default
constexpr bool IsConsumed(size_type global_index) const noexcept
Checks if a specific message is marked as consumed.
static constexpr TypeIndex From() noexcept
Constructs a TypeIndex from a type T.
Concept for consumable message types.
Definition message.hpp:86
utils::TypeIndex MessageTypeIndex
Type index for messages.
Definition message.hpp:14
helios::ecs::ConsumedMessagesRegistry< std::pmr::polymorphic_allocator< std::byte > > PmrConsumedMessagesRegistry
STL namespace.