Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
n_frame_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/core_pch.hpp>
4
9
10#include <algorithm>
11#include <array>
12#include <atomic>
13#include <concepts>
14#include <cstddef>
15#include <memory>
16#include <utility>
17
18namespace helios::memory {
19
20/**
21 * @brief N-buffered frame allocator.
22 * @details Maintains N frame buffers, allowing memory from the previous N-1 frames to remain valid.
23 * Useful for pipelined operations (e.g., CPU-GPU synchronization with multiple frames in flight).
24 *
25 * The allocator cycles through N buffers, ensuring that data from the previous
26 * N-1 frames remains accessible while allocating for the current frame.
27 *
28 * @note Thread-safe.
29 * Previous N-1 frames' data remains valid until the buffer cycles back.
30 *
31 * @tparam N Number of frame buffers (must be > 0)
32 */
33template <size_t N>
34 requires(N > 0)
35class NFrameAllocator final {
36public:
37 static constexpr size_t kBufferCount = N;
38
39 /**
40 * @brief Constructs an N-frame allocator with specified capacity per buffer.
41 * @warning Triggers assertion if capacity_per_buffer is 0.
42 * @param capacity_per_buffer Size of each buffer in bytes
43 */
44 explicit NFrameAllocator(size_t capacity_per_buffer) : allocators_(CreateAllocators(capacity_per_buffer)) {}
46 NFrameAllocator(NFrameAllocator&& other) noexcept;
47 ~NFrameAllocator() noexcept = default;
48
49 NFrameAllocator& operator=(const NFrameAllocator&) = delete;
50 NFrameAllocator& operator=(NFrameAllocator&& other) noexcept;
51
52 /**
53 * @brief Allocates memory from the current frame buffer.
54 * @warning Triggers assertion in next cases:
55 * - Alignment is not a power of 2.
56 * - Alignment is less than kMinAlignment.
57 * @param size Number of bytes to allocate
58 * @param alignment Alignment requirement (must be power of 2)
59 * @return AllocationResult with pointer and actual allocated size, or {nullptr, 0} on failure
60 */
61 [[nodiscard]] AllocationResult Allocate(size_t size, size_t alignment = kDefaultAlignment) noexcept {
62 return allocators_[current_buffer_.load(std::memory_order_acquire)].Allocate(size, alignment);
63 }
64
65 /**
66 * @brief Allocates memory for a single object of type T.
67 * @details Convenience function that calculates size and alignment from the type.
68 * The returned memory is uninitialized - use placement new to construct the object.
69 * @tparam T Type to allocate memory for
70 * @return Pointer to allocated memory, or nullptr on failure
71 */
72 template <typename T>
73 [[nodiscard]] T* Allocate() noexcept;
74
75 /**
76 * @brief Allocates memory for an array of objects of type T.
77 * @details Convenience function that calculates size and alignment from the type.
78 * The returned memory is uninitialized - use placement new to construct objects.
79 * @tparam T Type to allocate memory for
80 * @param count Number of objects to allocate space for
81 * @return Pointer to allocated memory, or nullptr on failure
82 */
83 template <typename T>
84 [[nodiscard]] T* Allocate(size_t count) noexcept;
85
86 /**
87 * @brief Allocates and constructs a single object of type T.
88 * @details Convenience function that allocates memory and constructs the object in-place.
89 * @tparam T Type to allocate and construct
90 * @tparam Args Constructor argument types
91 * @param args Arguments to forward to T's constructor
92 * @return Pointer to constructed object, or nullptr on allocation failure
93 */
94 template <typename T, typename... Args>
95 requires std::constructible_from<T, Args...>
96 [[nodiscard]] T* AllocateAndConstruct(Args&&... args) noexcept(std::is_nothrow_constructible_v<T, Args...>);
97
98 /**
99 * @brief Allocates and default-constructs an array of objects of type T.
100 * @details Convenience function that allocates memory and default-constructs objects in-place.
101 * @tparam T Type to allocate and construct (must be default constructible)
102 * @param count Number of objects to allocate and construct
103 * @return Pointer to first constructed object, or nullptr on allocation failure
104 */
105 template <typename T>
106 requires std::default_initializable<T>
107 [[nodiscard]] T* AllocateAndConstructArray(size_t count) noexcept(std::is_nothrow_default_constructible_v<T>);
108
109 /**
110 * @brief Advances to the next frame, cycling through buffers.
111 * @details Resets the new current buffer and makes previous buffers accessible.
112 * @warning Not thread-safe with Allocate(). Must be called from a single thread while
113 * no other threads are allocating. Typically called once per frame by the main thread.
114 */
115 void NextFrame() noexcept;
116
117 /**
118 * @brief Resets all buffers.
119 * @details Clears all allocations from all buffers.
120 */
121 void Reset() noexcept;
122
123 /**
124 * @brief Gets combined statistics for all buffers.
125 * @return AllocatorStats with combined usage information
126 */
127 [[nodiscard]] AllocatorStats Stats() const noexcept;
128
129 /**
130 * @brief Gets statistics for the current frame buffer.
131 * @return AllocatorStats for current buffer
132 */
133 [[nodiscard]] AllocatorStats CurrentFrameStats() const noexcept {
134 return allocators_[current_buffer_.load(std::memory_order_acquire)].Stats();
135 }
136
137 /**
138 * @brief Gets statistics for a specific buffer.
139 * @warning Triggers assertion if buffer_index is out of range [0, N).
140 * @param buffer_index Buffer index (0 to N-1)
141 * @return AllocatorStats for specified buffer
142 */
143 [[nodiscard]] AllocatorStats BufferStats(size_t buffer_index) const noexcept;
144
145 /**
146 * @brief Gets the total capacity across all buffers.
147 * @return Total capacity in bytes
148 */
149 [[nodiscard]] size_t Capacity() const noexcept;
150
151 /**
152 * @brief Gets the current frame buffer index.
153 * @return Current buffer index (0 to N-1)
154 */
155 [[nodiscard]] size_t CurrentBufferIndex() const noexcept { return current_buffer_.load(std::memory_order_relaxed); }
156
157 /**
158 * @brief Gets free space in current buffer.
159 * @return Free space in bytes
160 */
161 [[nodiscard]] size_t FreeSpace() const noexcept {
162 return allocators_[current_buffer_.load(std::memory_order_acquire)].FreeSpace();
163 }
164
165 /**
166 * @brief Gets the number of buffers.
167 * @return Number of buffers (N)
168 */
169 [[nodiscard]] static constexpr size_t BufferCount() noexcept { return N; }
170
171private:
172 /**
173 * @brief Helper to create array of allocators.
174 */
175 [[nodiscard]] static auto CreateAllocators(size_t capacity_per_buffer) -> std::array<FrameAllocator, N>;
176
177 std::array<FrameAllocator, N> allocators_; ///< N frame allocators
178 std::atomic<size_t> current_buffer_{0}; ///< Current buffer index
179};
180
181template <size_t N>
182 requires(N > 0)
184 : allocators_(std::move(other.allocators_)),
185 current_buffer_(other.current_buffer_.load(std::memory_order_acquire)) {
186 other.current_buffer_.store(0, std::memory_order_release);
187}
188
189template <size_t N>
190 requires(N > 0)
192 if (this == &other) [[unlikely]] {
193 return *this;
194 }
195
196 allocators_ = std::move(other.allocators_);
197 current_buffer_.store(other.current_buffer_.load(std::memory_order_acquire), std::memory_order_release);
198 other.current_buffer_.store(0, std::memory_order_release);
199
200 return *this;
201}
202
203template <size_t N>
204 requires(N > 0)
205template <typename T>
206inline T* NFrameAllocator<N>::Allocate() noexcept {
207 constexpr size_t size = sizeof(T);
208 constexpr size_t alignment = std::max(alignof(T), kMinAlignment);
209 auto result = Allocate(size, alignment);
210 return static_cast<T*>(result.ptr);
211}
212
213template <size_t N>
214 requires(N > 0)
215template <typename T>
216inline T* NFrameAllocator<N>::Allocate(size_t count) noexcept {
217 if (count == 0) [[unlikely]] {
218 return nullptr;
219 }
220 constexpr size_t alignment = std::max(alignof(T), kMinAlignment);
221 const size_t size = sizeof(T) * count;
222 auto result = Allocate(size, alignment);
223 return static_cast<T*>(result.ptr);
224}
225
226template <size_t N>
227 requires(N > 0)
228template <typename T, typename... Args>
229 requires std::constructible_from<T, Args...>
230inline T* NFrameAllocator<N>::AllocateAndConstruct(Args&&... args) noexcept(
231 std::is_nothrow_constructible_v<T, Args...>) {
232 T* ptr = Allocate<T>();
233 if (ptr != nullptr) [[likely]] {
234 std::construct_at(ptr, std::forward<Args>(args)...);
235 }
236 return ptr;
237}
238
239template <size_t N>
240 requires(N > 0)
241template <typename T>
242 requires std::default_initializable<T>
243inline T* NFrameAllocator<N>::AllocateAndConstructArray(size_t count) noexcept(
244 std::is_nothrow_default_constructible_v<T>) {
245 T* ptr = Allocate<T>(count);
246 if (ptr != nullptr) [[likely]] {
247 for (size_t i = 0; i < count; ++i) {
248 std::construct_at(ptr + i);
249 }
250 }
251 return ptr;
252}
253
254template <size_t N>
255 requires(N > 0)
256inline void NFrameAllocator<N>::NextFrame() noexcept {
257 // Advance to next buffer (wrapping around)
258 const size_t buffer = (current_buffer_.load(std::memory_order_relaxed) + 1) % N;
259
260 // Reset the new current buffer before switching
261 allocators_[buffer].Reset();
262
263 // Switch to new buffer
264 current_buffer_.store(buffer, std::memory_order_release);
265}
266
267template <size_t N>
268 requires(N > 0)
269inline void NFrameAllocator<N>::Reset() noexcept {
270 for (auto& allocator : allocators_) {
271 allocator.Reset();
272 }
273}
274
275template <size_t N>
276 requires(N > 0)
278 AllocatorStats combined{};
279
280 for (const auto& allocator : allocators_) {
281 const auto stats = allocator.Stats();
282 combined.total_allocated += stats.total_allocated;
283 combined.total_freed += stats.total_freed;
284 combined.peak_usage = std::max(combined.peak_usage, stats.peak_usage);
285 combined.allocation_count += stats.allocation_count;
286 combined.total_allocations += stats.total_allocations;
287 combined.total_deallocations += stats.total_deallocations;
288 combined.alignment_waste += stats.alignment_waste;
289 }
290
291 return combined;
292}
293
294template <size_t N>
295 requires(N > 0)
296inline AllocatorStats NFrameAllocator<N>::BufferStats(size_t buffer_index) const noexcept {
297 HELIOS_ASSERT(buffer_index < N, "Failed to get buffer stats: buffer_index '{}' is out of range [0, {}]!",
298 buffer_index, N);
299 return allocators_[buffer_index].Stats();
300}
301
302template <size_t N>
303 requires(N > 0)
304inline size_t NFrameAllocator<N>::Capacity() const noexcept {
305 size_t total = 0;
306 for (const auto& allocator : allocators_) {
307 total += allocator.Capacity();
308 }
309 return total;
310}
311
312template <size_t N>
313 requires(N > 0)
314inline auto NFrameAllocator<N>::CreateAllocators(size_t capacity_per_buffer) -> std::array<FrameAllocator, N> {
315 // Helper to construct array with compile-time recursion
316 auto construct = [capacity_per_buffer]<size_t... Is>(std::index_sequence<Is...>) -> std::array<FrameAllocator, N> {
317 return {(static_cast<void>(Is), FrameAllocator(capacity_per_buffer))...};
318 };
319 return construct(std::make_index_sequence<N>{});
320}
321
322using TripleFrameAllocator = NFrameAllocator<3>; ///< Triple-buffered frame allocator
323using QuadFrameAllocator = NFrameAllocator<4>; ///< Quad-buffered frame allocator
324
325} // namespace helios::memory
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:140
Linear allocator that clears every frame.
N-buffered frame allocator.
static constexpr size_t BufferCount() noexcept
Gets the number of buffers.
~NFrameAllocator() noexcept=default
size_t Capacity() const noexcept
Gets the total capacity across all buffers.
size_t FreeSpace() const noexcept
Gets free space in current buffer.
void Reset() noexcept
Resets all buffers.
NFrameAllocator(const NFrameAllocator &)=delete
NFrameAllocator(size_t capacity_per_buffer)
Constructs an N-frame allocator with specified capacity per buffer.
constexpr size_t kDefaultAlignment
Default alignment for allocations (cache line size for most modern CPUs).
constexpr size_t kMinAlignment
Minimum alignment for any allocation.
constexpr T * AllocateAndConstructArray(Alloc &allocator, size_t count) noexcept(std::is_nothrow_default_constructible_v< T >)
constexpr T * Allocate(Alloc &allocator) noexcept
constexpr T * AllocateAndConstruct(Alloc &allocator, Args &&... args) noexcept(std::is_nothrow_constructible_v< T, Args... >)
STL namespace.
Result type for allocation operations.
Statistics for tracking allocator usage.
size_t total_allocated
Total bytes currently allocated.