Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
double_frame_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/core_pch.hpp>
4
7
8#include <algorithm>
9#include <array>
10#include <atomic>
11#include <concepts>
12#include <cstddef>
13#include <memory>
14#include <utility>
15
16namespace helios::memory {
17
18/**
19 * @brief Double-buffered frame allocator.
20 * @details Maintains two frame buffers,
21 * allowing memory from the previous frame to remain valid while allocating for the current frame.
22 * Useful when data needs to be accessible for one additional frame (e.g., GPU upload buffers, interpolation).
23 *
24 * The allocator automatically switches between buffers on each frame.
25 *
26 * Uses atomic for buffer index and frame transitions.
27 * Previous frame's data remains valid until the next frame begins.
28 * Allocations are lock-free since FrameAllocator uses atomic operations internally.
29 *
30 * @note Thread-safe.
31 */
33public:
34 static constexpr size_t kBufferCount = 2;
35
36 /**
37 * @brief Constructs a double frame allocator with specified capacity per buffer.
38 * @warning Triggers assertion if capacity_per_buffer is 0.
39 * @param capacity_per_buffer Size of each buffer in bytes
40 */
41 explicit DoubleFrameAllocator(size_t capacity_per_buffer)
42 : allocators_{FrameAllocator(capacity_per_buffer), FrameAllocator(capacity_per_buffer)} {}
43
46 ~DoubleFrameAllocator() noexcept = default;
47
48 DoubleFrameAllocator& operator=(const DoubleFrameAllocator&) = delete;
49 DoubleFrameAllocator& operator=(DoubleFrameAllocator&& other) noexcept;
50
51 /**
52 * @brief Allocates memory from the current frame buffer.
53 * @details This operation is lock-free as it only reads the atomic buffer index
54 * and delegates to the thread-safe FrameAllocator.
55 * @warning Triggers assertion in next cases:
56 * - Alignment is not a power of 2.
57 * - Alignment is less than kMinAlignment.
58 * @param size Number of bytes to allocate
59 * @param alignment Alignment requirement (must be power of 2)
60 * @return AllocationResult with pointer and actual allocated size, or {nullptr, 0} on failure
61 */
62 [[nodiscard]] AllocationResult Allocate(size_t size, size_t alignment = kDefaultAlignment) noexcept;
63
64 /**
65 * @brief Allocates memory for a single object of type T.
66 * @details Convenience function that calculates size and alignment from the type.
67 * The returned memory is uninitialized - use placement new to construct the object.
68 * @tparam T Type to allocate memory for
69 * @return Pointer to allocated memory, or nullptr on failure
70 */
71 template <typename T>
72 [[nodiscard]] T* Allocate() noexcept;
73
74 /**
75 * @brief Allocates memory for an array of objects of type T.
76 * @details Convenience function that calculates size and alignment from the type.
77 * The returned memory is uninitialized - use placement new to construct objects.
78 * @tparam T Type to allocate memory for
79 * @param count Number of objects to allocate space for
80 * @return Pointer to allocated memory, or nullptr on failure
81 */
82 template <typename T>
83 [[nodiscard]] T* Allocate(size_t count) noexcept;
84
85 /**
86 * @brief Allocates and constructs a single object of type T.
87 * @details Convenience function that allocates memory and constructs the object in-place.
88 * @tparam T Type to allocate and construct
89 * @tparam Args Constructor argument types
90 * @param args Arguments to forward to T's constructor
91 * @return Pointer to constructed object, or nullptr on allocation failure
92 */
93 template <typename T, typename... Args>
94 requires std::constructible_from<T, Args...>
95 [[nodiscard]] T* AllocateAndConstruct(Args&&... args) noexcept(std::is_nothrow_constructible_v<T, Args...>);
96
97 /**
98 * @brief Allocates and default-constructs an array of objects of type T.
99 * @details Convenience function that allocates memory and default-constructs objects in-place.
100 * @tparam T Type to allocate and construct (must be default constructible)
101 * @param count Number of objects to allocate and construct
102 * @return Pointer to first constructed object, or nullptr on allocation failure
103 */
104 template <typename T>
105 requires std::default_initializable<T>
106 [[nodiscard]] T* AllocateAndConstructArray(size_t count) noexcept(std::is_nothrow_default_constructible_v<T>);
107
108 /**
109 * @brief Advances to the next frame, switching buffers.
110 * @details Resets the new current buffer and makes the old current buffer the previous buffer.
111 * @warning Not thread-safe with Allocate().
112 * Must be called from a single thread while no other threads are allocating.
113 * Typically called once per frame by the main thread.
114 */
115 void NextFrame() noexcept;
116
117 /**
118 * @brief Resets both buffers.
119 * @details Clears all allocations from both buffers.
120 */
121 void Reset() noexcept;
122
123 /**
124 * @brief Gets combined statistics for both 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
135 /**
136 * @brief Gets statistics for the previous frame buffer.
137 * @return AllocatorStats for previous buffer
138 */
139 [[nodiscard]] AllocatorStats PreviousFrameStats() const noexcept;
140
141 /**
142 * @brief Gets the total capacity across both buffers.
143 * @return Total capacity in bytes
144 */
145 [[nodiscard]] size_t Capacity() const noexcept {
146 return allocators_.front().Capacity() + allocators_.back().Capacity();
147 }
148
149 /**
150 * @brief Gets the current frame buffer index.
151 * @return Current buffer index (0 or 1)
152 */
153 [[nodiscard]] size_t CurrentBufferIndex() const noexcept { return current_buffer_.load(std::memory_order_relaxed); }
154
155 /**
156 * @brief Gets the previous frame buffer index.
157 * @return Previous buffer index (0 or 1)
158 */
159 [[nodiscard]] size_t PreviousBufferIndex() const noexcept {
160 return 1 - current_buffer_.load(std::memory_order_relaxed);
161 }
162
163 /**
164 * @brief Gets free space in current buffer.
165 * @return Free space in bytes
166 */
167 [[nodiscard]] size_t FreeSpace() const noexcept;
168
169private:
170 std::array<FrameAllocator, kBufferCount> allocators_; ///< Two frame allocators
171 std::atomic<size_t> current_buffer_{0}; ///< Current buffer index (atomic for lock-free reads)
172};
173
175 : allocators_(std::move(other.allocators_)),
176 current_buffer_(other.current_buffer_.load(std::memory_order_acquire)) {
177 other.current_buffer_.store(0, std::memory_order_release);
178}
179
181 if (this == &other) [[unlikely]] {
182 return *this;
183 }
184
185 allocators_ = std::move(other.allocators_);
186
187 current_buffer_.store(other.current_buffer_.load(std::memory_order_acquire), std::memory_order_release);
188 other.current_buffer_.store(0, std::memory_order_release);
189
190 return *this;
191}
192
193inline AllocationResult DoubleFrameAllocator::Allocate(size_t size, size_t alignment) noexcept {
194 // Lock-free: current_buffer_ is atomic, FrameAllocator::Allocate is thread-safe via atomics
195 const size_t buffer = current_buffer_.load(std::memory_order_acquire);
196 return allocators_[buffer].Allocate(size, alignment);
197}
198
199inline void DoubleFrameAllocator::NextFrame() noexcept {
200 // Switch to the other buffer
201 const size_t new_buffer = 1 - current_buffer_.load(std::memory_order_relaxed);
202
203 // Reset the new current buffer before switching
204 allocators_[new_buffer].Reset();
205
206 // Switch to new buffer
207 current_buffer_.store(new_buffer, std::memory_order_release);
208}
209
210inline void DoubleFrameAllocator::Reset() noexcept {
211 allocators_.front().Reset();
212 allocators_.back().Reset();
213}
214
216 const AllocatorStats stats0 = allocators_.front().Stats();
217 const AllocatorStats stats1 = allocators_.back().Stats();
218
219 return {
220 .total_allocated = stats0.total_allocated + stats1.total_allocated,
221 .total_freed = stats0.total_freed + stats1.total_freed,
222 .peak_usage = std::max(stats0.peak_usage, stats1.peak_usage),
223 .allocation_count = stats0.allocation_count + stats1.allocation_count,
224 .total_allocations = stats0.total_allocations + stats1.total_allocations,
225 .total_deallocations = stats0.total_deallocations + stats1.total_deallocations,
226 .alignment_waste = stats0.alignment_waste + stats1.alignment_waste,
227 };
228}
229
230template <typename T>
231inline T* DoubleFrameAllocator::Allocate() noexcept {
232 constexpr size_t size = sizeof(T);
233 constexpr size_t alignment = std::max(alignof(T), kMinAlignment);
234 auto result = Allocate(size, alignment);
235 return static_cast<T*>(result.ptr);
236}
237
238template <typename T>
239inline T* DoubleFrameAllocator::Allocate(size_t count) noexcept {
240 if (count == 0) [[unlikely]] {
241 return nullptr;
242 }
243 constexpr size_t alignment = std::max(alignof(T), kMinAlignment);
244 const size_t size = sizeof(T) * count;
245 auto result = Allocate(size, alignment);
246 return static_cast<T*>(result.ptr);
247}
248
249template <typename T, typename... Args>
250 requires std::constructible_from<T, Args...>
251inline T* DoubleFrameAllocator::AllocateAndConstruct(Args&&... args) noexcept(
252 std::is_nothrow_constructible_v<T, Args...>) {
253 T* ptr = Allocate<T>();
254 if (ptr != nullptr) [[likely]] {
255 std::construct_at(ptr, std::forward<Args>(args)...);
256 }
257 return ptr;
258}
259
260template <typename T>
261 requires std::default_initializable<T>
262inline T* DoubleFrameAllocator::AllocateAndConstructArray(size_t count) noexcept(
263 std::is_nothrow_default_constructible_v<T>) {
264 T* ptr = Allocate<T>(count);
265 if (ptr != nullptr) [[likely]] {
266 for (size_t i = 0; i < count; ++i) {
267 std::construct_at(ptr + i);
268 }
269 }
270 return ptr;
271}
272
274 const size_t buffer = current_buffer_.load(std::memory_order_relaxed);
275 return allocators_[buffer].Stats();
276}
277
279 const size_t buffer = current_buffer_.load(std::memory_order_relaxed);
280 return allocators_[1 - buffer].Stats();
281}
282
283inline size_t DoubleFrameAllocator::FreeSpace() const noexcept {
284 const size_t buffer = current_buffer_.load(std::memory_order_relaxed);
285 return allocators_[buffer].FreeSpace();
286}
287
288} // namespace helios::memory
DoubleFrameAllocator(const DoubleFrameAllocator &)=delete
DoubleFrameAllocator & operator=(const DoubleFrameAllocator &)=delete
size_t CurrentBufferIndex() const noexcept
Gets the current frame buffer index.
void NextFrame() noexcept
Advances to the next frame, switching buffers.
size_t FreeSpace() const noexcept
Gets free space in current buffer.
T * AllocateAndConstruct(Args &&... args) noexcept(std::is_nothrow_constructible_v< T, Args... >)
Allocates and constructs a single object of type T.
T * Allocate() noexcept
Allocates memory for a single object of type T.
size_t Capacity() const noexcept
Gets the total capacity across both buffers.
~DoubleFrameAllocator() noexcept=default
AllocatorStats PreviousFrameStats() const noexcept
Gets statistics for the previous frame buffer.
void Reset() noexcept
Resets both buffers.
T * AllocateAndConstructArray(size_t count) noexcept(std::is_nothrow_default_constructible_v< T >)
Allocates and default-constructs an array of objects of type T.
AllocatorStats CurrentFrameStats() const noexcept
Gets statistics for the current frame buffer.
DoubleFrameAllocator(size_t capacity_per_buffer)
Constructs a double frame allocator with specified capacity per buffer.
AllocatorStats Stats() const noexcept
Gets combined statistics for both buffers.
size_t PreviousBufferIndex() const noexcept
Gets the previous frame buffer index.
Linear allocator that clears every frame.
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 * Allocate(Alloc &allocator) noexcept
STL namespace.
Result type for allocation operations.
Statistics for tracking allocator usage.
size_t total_freed
Total bytes freed.
size_t peak_usage
Peak memory usage.
size_t alignment_waste
Bytes wasted due to alignment.
size_t allocation_count
Number of active allocations.
size_t total_allocations
Total number of allocations made.
size_t total_allocated
Total bytes currently allocated.
size_t total_deallocations
Total number of deallocations made.