Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
frame_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
6#include <helios/memory/details/profile.hpp>
7
8#include <algorithm>
9#include <array>
10#include <cstddef>
11#include <memory_resource>
12#include <utility>
13
14namespace helios::mem {
15
16/**
17 * @brief Configuration for `FrameAllocator`.
18 * @details Controls per-frame arena capacity and growth behavior.
19 */
21 /// @brief Initial per-frame arena capacity in bytes.
22 size_t initial_capacity = 0;
23 /// @brief Growth policy applied to each frame arena.
25};
26
27/**
28 * @brief N-buffered PMR frame allocator.
29 * @details Holds N arena resources and routes allocations to current frame.
30 *
31 * `Advance()` switches to the next frame and resets it, allowing memory reuse
32 * with deterministic lifetime windows.
33 *
34 * @note Allocation is thread-safe as delegated to ArenaAllocator.
35 * @warning `Advance()` and `Reset()` must not run concurrently with allocate.
36 * @tparam N Number of frame buffers, must be >= 1
37 */
38template <size_t N = 2>
39 requires(N >= 1)
40class FrameAllocator final : public std::pmr::memory_resource {
41public:
42 /**
43 * @brief Constructs N-frame allocator from options.
44 * @warning Triggers assertion if `options.initial_capacity == 0` or
45 * `options.growth.max_capacity < options.initial_capacity`.
46 * @param options Frame allocator options
47 */
48 explicit FrameAllocator(FrameAllocatorOptions options) noexcept;
49
50 /**
51 * @brief Constructs N-frame allocator with geometrically growing arenas.
52 * @warning Triggers assertion if `initial_capacity == 0`.
53 * @param initial_capacity Initial per-frame arena capacity
54 */
55 explicit FrameAllocator(size_t initial_capacity) noexcept
57 FrameAllocatorOptions{.initial_capacity = initial_capacity,
58 .growth = GrowthPolicy::Geometric()}) {}
59
61
62 /**
63 * @brief Move-constructs frame allocator from another instance.
64 * @param other Source allocator; frame index and arenas are transferred
65 */
67 : arenas_(std::move(other.arenas_)) {
68 MoveFrom(other);
69 }
70
71 ~FrameAllocator() noexcept override = default;
72
73 FrameAllocator& operator=(const FrameAllocator&) = delete;
74
75 /**
76 * @brief Move-assigns frame allocator state from another instance.
77 * @param other Source allocator; left in default moved-from state
78 * @return Reference to this allocator
79 */
80 FrameAllocator& operator=(FrameAllocator&& other) noexcept;
81
82 /**
83 * @brief Advances to the next frame and resets its arena.
84 * @details Wraps the frame index modulo `N`. The newly current arena is
85 * soft-reset, reclaiming all prior allocations in that slot.
86 * @warning Must not be called concurrently with `allocate` or `deallocate`.
87 */
88 void Advance() noexcept;
89
90 /**
91 * @brief Resets all frame arenas and returns to frame zero.
92 * @warning Must not be called concurrently with `allocate` or `deallocate`.
93 */
94 void Reset() noexcept;
95
96 /**
97 * @brief Returns true when current frame arena has no allocations.
98 * @return True if current arena is empty, false otherwise
99 */
100 [[nodiscard]] bool Empty() const noexcept { return CurrentArena().Empty(); }
101
102 /**
103 * @brief Returns stats for current frame arena.
104 * @return Allocator stats
105 */
106 [[nodiscard]] AllocatorStats Stats() const noexcept {
107 return CurrentArena().Stats();
108 }
109
110 /**
111 * @brief Returns aggregated stats across all frame arenas.
112 * @return Summed allocator stats
113 */
114 [[nodiscard]] AllocatorStats AggregateStats() const noexcept;
115
116 /**
117 * @brief Returns current frame index.
118 * @return Current frame in range [0, N)
119 */
120 [[nodiscard]] size_t FrameIndex() const noexcept { return frame_index_; }
121
122 /**
123 * @brief Returns number of frame buffers.
124 * @return N
125 */
126 [[nodiscard]] static consteval size_t FrameCount() noexcept { return N; }
127
128 /**
129 * @brief Returns per-arena initial capacity.
130 * @return Capacity in bytes
131 */
132 [[nodiscard]] size_t InitialCapacity() const noexcept {
133 return initial_capacity_;
134 }
135
136 /**
137 * @brief Returns growth policy.
138 * @return Growth policy
139 */
140 [[nodiscard]] GrowthPolicy Growth() const noexcept { return growth_; }
141
142 /**
143 * @brief Returns current arena total capacity.
144 * @return Capacity in bytes
145 */
146 [[nodiscard]] size_t TotalCapacity() const noexcept {
147 return CurrentArena().TotalCapacity();
148 }
149
150 /**
151 * @brief Returns current arena block count.
152 * @return Block count
153 */
154 [[nodiscard]] size_t BlockCount() const noexcept {
155 return CurrentArena().BlockCount();
156 }
157
158 /**
159 * @brief Returns mutable arena by frame index.
160 * @warning Triggers assertion if `index >= N`.
161 * @param index Arena index
162 * @return Arena reference
163 */
164 [[nodiscard]] ArenaAllocator& Arena(size_t index) noexcept;
165
166 /**
167 * @brief Returns const arena by frame index.
168 * @warning Triggers assertion if `index >= N`.
169 * @param index Arena index
170 * @return Const arena reference
171 */
172 [[nodiscard]] const ArenaAllocator& Arena(size_t index) const noexcept;
173
174private:
175 [[nodiscard]] ArenaAllocator& CurrentArena() noexcept {
176 return arenas_[frame_index_];
177 }
178
179 [[nodiscard]] const ArenaAllocator& CurrentArena() const noexcept {
180 return arenas_[frame_index_];
181 }
182
183 void MoveFrom(FrameAllocator& other) noexcept;
184
185 template <size_t... Indexes>
186 [[nodiscard]] static auto BuildArenas(const FrameAllocatorOptions& options,
187 std::index_sequence<Indexes...> /*seq*/)
188 -> std::array<ArenaAllocator, N>;
189
190 [[nodiscard]] void* do_allocate(size_t bytes, size_t alignment) override {
191 HELIOS_MEMORY_PROFILE_SCOPE();
192 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
193
194 return CurrentArena().allocate(bytes, alignment);
195 }
196
197 void do_deallocate(void* ptr, size_t bytes, size_t alignment) override {
198 HELIOS_MEMORY_PROFILE_SCOPE();
199 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
200
201 CurrentArena().deallocate(ptr, bytes, alignment);
202 }
203
204 [[nodiscard]] bool do_is_equal(
205 const std::pmr::memory_resource& other) const noexcept override {
206 return this == &other;
207 }
208
209 std::array<ArenaAllocator, N> arenas_;
210 size_t initial_capacity_ = 0;
211 GrowthPolicy growth_;
212 size_t frame_index_ = 0;
213};
214
215template <size_t N>
216 requires(N >= 1)
218 : arenas_(BuildArenas(options, std::make_index_sequence<N>{})),
219 initial_capacity_(options.initial_capacity),
220 growth_(options.growth) {
221 HELIOS_ASSERT(initial_capacity_ > 0,
222 "initial_capacity must be greater than zero!");
223}
224
225template <size_t N>
226 requires(N >= 1)
228 FrameAllocator&& other) noexcept {
229 if (this == &other) [[unlikely]] {
230 return *this;
231 }
232
233 arenas_ = std::move(other.arenas_);
234 MoveFrom(other);
235 return *this;
236}
237
238template <size_t N>
239 requires(N >= 1)
240inline void FrameAllocator<N>::MoveFrom(FrameAllocator& other) noexcept {
241 initial_capacity_ = std::exchange(other.initial_capacity_, 0);
242 growth_ = other.growth_;
243 frame_index_ = std::exchange(other.frame_index_, 0);
244}
245
246template <size_t N>
247 requires(N >= 1)
248inline void FrameAllocator<N>::Advance() noexcept {
249 HELIOS_MEMORY_PROFILE_SCOPE();
250 HELIOS_MEMORY_PROFILE_ZONE_VALUE(frame_index_);
251
252 frame_index_ = (frame_index_ + 1) % N;
253 CurrentArena().Reset();
254}
255
256template <size_t N>
257 requires(N >= 1)
258inline void FrameAllocator<N>::Reset() noexcept {
259 HELIOS_MEMORY_PROFILE_SCOPE();
260
261 for (auto& arena : arenas_) {
262 arena.Reset();
263 }
264 frame_index_ = 0;
265}
266
267template <size_t N>
268 requires(N >= 1)
270 AllocatorStats aggregate{};
271 for (const auto& arena : arenas_) {
272 const AllocatorStats stats = arena.Stats();
273 aggregate.total_allocated =
275 aggregate.peak_usage =
276 SaturatingAdd(aggregate.peak_usage, stats.peak_usage);
277 aggregate.allocation_count =
279 aggregate.total_allocations =
281 aggregate.total_deallocations =
283 aggregate.alignment_waste =
285 }
286 return aggregate;
287}
288
289template <size_t N>
290 requires(N >= 1)
291inline ArenaAllocator& FrameAllocator<N>::Arena(size_t index) noexcept {
292 HELIOS_ASSERT(index < N, "index '{}' is out of range [0, {})!", index, N);
293 return arenas_[index];
294}
295
296template <size_t N>
297 requires(N >= 1)
299 size_t index) const noexcept {
300 HELIOS_ASSERT(index < N, "index '{}' is out of range [0, {})!", index, N);
301 return arenas_[index];
302}
303
304template <size_t N>
305 requires(N >= 1)
306template <size_t... Indexes>
307inline auto FrameAllocator<N>::BuildArenas(
308 const FrameAllocatorOptions& options,
309 std::index_sequence<Indexes...> /*seq*/) -> std::array<ArenaAllocator, N> {
310 return {((void)Indexes, ArenaAllocator(ArenaOptions{
311 .initial_capacity = options.initial_capacity,
312 .growth = options.growth}))...};
313}
314
315} // namespace helios::mem
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
PMR arena allocator with lock-free hot allocation path.
N-buffered PMR frame allocator.
void Reset() noexcept
Resets all frame arenas and returns to frame zero.
FrameAllocator(FrameAllocator &&other) noexcept
Move-constructs frame allocator from another instance.
size_t FrameIndex() const noexcept
Returns current frame index.
bool Empty() const noexcept
Returns true when current frame arena has no allocations.
GrowthPolicy Growth() const noexcept
Returns growth policy.
FrameAllocator(size_t initial_capacity) noexcept
Constructs N-frame allocator with geometrically growing arenas.
FrameAllocator(const FrameAllocator &)=delete
ArenaAllocator & Arena(size_t index) noexcept
Returns mutable arena by frame index.
~FrameAllocator() noexcept override=default
size_t BlockCount() const noexcept
Returns current arena block count.
AllocatorStats Stats() const noexcept
Returns stats for current frame arena.
AllocatorStats AggregateStats() const noexcept
Returns aggregated stats across all frame arenas.
FrameAllocator(FrameAllocatorOptions options) noexcept
Constructs N-frame allocator from options.
void Advance() noexcept
Advances to the next frame and resets its arena.
size_t InitialCapacity() const noexcept
Returns per-arena initial capacity.
static consteval size_t FrameCount() noexcept
Returns number of frame buffers.
size_t TotalCapacity() const noexcept
Returns current arena total capacity.
constexpr size_t SaturatingAdd(size_t lhs, size_t rhs) noexcept
Saturating add for size_t.
Definition common.hpp:25
STL namespace.
Runtime statistics snapshot for PMR allocators.
Definition common.hpp:163
size_t total_allocated
Bytes currently reserved or bumped (allocator-specific semantics).
Definition common.hpp:165
size_t peak_usage
High-water mark of total_allocated.
Definition common.hpp:167
size_t total_deallocations
Total deallocation calls (including arena no-op deallocates).
Definition common.hpp:173
size_t alignment_waste
Padding bytes consumed for alignment.
Definition common.hpp:175
size_t total_allocations
Total successful allocation calls.
Definition common.hpp:171
size_t allocation_count
Number of live allocations tracked by the allocator.
Definition common.hpp:169
Configuration for ArenaAllocator.
Configuration for FrameAllocator.
GrowthPolicy growth
Growth policy applied to each frame arena.
size_t initial_capacity
Initial per-frame arena capacity in bytes.
Capacity growth configuration.
Definition common.hpp:60
static constexpr GrowthPolicy Geometric(size_t numerator=2, size_t denominator=1, size_t max=std::numeric_limits< size_t >::max()) noexcept
Creates a geometric growth policy.
Definition common.hpp:96