Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
frame_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/core_pch.hpp>
4
9
10#include <atomic>
11#include <concepts>
12#include <cstddef>
13#include <cstdint>
14#include <cstring>
15#include <memory>
16
17namespace helios::memory {
18
19/**
20 * @brief Linear allocator that clears every frame.
21 * @details Fast bump-pointer allocator for per-frame temporary allocations.
22 * Extremely efficient for short-lived allocations that don't need individual deallocation.
23 * All memory is freed at once when Reset() is called (typically at frame end).
24 *
25 * Uses atomic operations for allocation offset tracking.
26 *
27 * Ideal for temporary data that lives for a single frame.
28 *
29 * @note Thread-safe.
30 * Deallocation is a no-op - memory is only freed on Reset().
31 * @warning Data allocated with this allocator is only valid until Reset() is called.
32 * All pointers and references to allocated memory become invalid after Reset().
33 * Do not store frame-allocated data in persistent storage (components, resources, etc.).
34 */
35class FrameAllocator final {
36public:
37 /**
38 * @brief Constructs a frame allocator with specified capacity.
39 * @warning Triggers assertion if capacity is 0.
40 * @param capacity Total size of the memory buffer in bytes
41 */
42 explicit FrameAllocator(size_t capacity);
44 FrameAllocator(FrameAllocator&& other) noexcept;
45 ~FrameAllocator() noexcept;
46
47 FrameAllocator& operator=(const FrameAllocator&) = delete;
48 FrameAllocator& operator=(FrameAllocator&& other) noexcept;
49
50 /**
51 * @brief Allocates memory with specified size and alignment.
52 * @warning Triggers assertion in next cases:
53 * - Alignment is not a power of 2.
54 * - Alignment is less than kMinAlignment.
55 * @param size Number of bytes to allocate
56 * @param alignment Alignment requirement (must be power of 2)
57 * @return AllocationResult with pointer and actual allocated size, or {nullptr, 0} on failure
58 */
59 [[nodiscard]] AllocationResult Allocate(size_t size, size_t alignment = kDefaultAlignment) noexcept;
60
61 /**
62 * @brief Allocates memory for a single object of type T.
63 * @details Convenience function that calculates size and alignment from the type.
64 * The returned memory is uninitialized - use placement new to construct the object.
65 * @tparam T Type to allocate memory for
66 * @return Pointer to allocated memory, or nullptr on failure
67 *
68 * @example
69 * @code
70 * FrameAllocator alloc(1024);
71 * int* ptr = alloc.Allocate<int>();
72 * if (ptr != nullptr) {
73 * new (ptr) int(42);
74 * }
75 * @endcode
76 */
77 template <typename T>
78 [[nodiscard]] T* Allocate() noexcept;
79
80 /**
81 * @brief Allocates memory for an array of objects of type T.
82 * @details Convenience function that calculates size and alignment from the type.
83 * The returned memory is uninitialized - use placement new to construct objects.
84 * @tparam T Type to allocate memory for
85 * @param count Number of objects to allocate space for
86 * @return Pointer to allocated memory, or nullptr on failure
87 *
88 * @example
89 * @code
90 * FrameAllocator alloc(1024);
91 * int* arr = alloc.Allocate<int>(10);
92 * @endcode
93 */
94 template <typename T>
95 [[nodiscard]] T* Allocate(size_t count) noexcept;
96
97 /**
98 * @brief Allocates and constructs a single object of type T.
99 * @details Convenience function that allocates memory and constructs the object in-place.
100 * @tparam T Type to allocate and construct
101 * @tparam Args Constructor argument types
102 * @param args Arguments to forward to T's constructor
103 * @return Pointer to constructed object, or nullptr on allocation failure
104 *
105 * @example
106 * @code
107 * FrameAllocator alloc(1024);
108 * auto* vec = alloc.AllocateAndConstruct<MyVec3>(1.0f, 2.0f, 3.0f);
109 * @endcode
110 */
111 template <typename T, typename... Args>
112 requires std::constructible_from<T, Args...>
113 [[nodiscard]] T* AllocateAndConstruct(Args&&... args) noexcept(std::is_nothrow_constructible_v<T, Args...>);
114
115 /**
116 * @brief Allocates and default-constructs an array of objects of type T.
117 * @details Convenience function that allocates memory and default-constructs objects in-place.
118 * @tparam T Type to allocate and construct (must be default constructible)
119 * @param count Number of objects to allocate and construct
120 * @return Pointer to first constructed object, or nullptr on allocation failure
121 *
122 * @example
123 * @code
124 * FrameAllocator alloc(1024);
125 * auto* arr = alloc.AllocateAndConstructArray<MyType>(10);
126 * @endcode
127 */
128 template <typename T>
129 requires std::default_initializable<T>
130 [[nodiscard]] T* AllocateAndConstructArray(size_t count) noexcept(std::is_nothrow_default_constructible_v<T>);
131
132 /**
133 * @brief Resets the allocator, freeing all allocations.
134 * @details Resets the internal offset to 0, effectively freeing all memory.
135 * Does not actually free or zero the underlying buffer.
136 * @warning All pointers obtained from this allocator become invalid after this call.
137 * Do not store references or pointers to frame-allocated data beyond the current frame.
138 */
139 void Reset() noexcept;
140
141 /**
142 * @brief Checks if the allocator is empty.
143 * @return True if no allocations have been made since last reset
144 */
145 [[nodiscard]] bool Empty() const noexcept { return offset_.load(std::memory_order_relaxed) == 0; }
146
147 /**
148 * @brief Checks if the allocator is full.
149 * @return True if no more allocations can be made without reset
150 */
151 [[nodiscard]] bool Full() const noexcept { return offset_.load(std::memory_order_relaxed) >= capacity_; }
152
153 /**
154 * @brief Gets current allocator statistics.
155 * @return AllocatorStats with current usage information
156 */
157 [[nodiscard]] AllocatorStats Stats() const noexcept;
158
159 /**
160 * @brief Gets the total capacity of the allocator.
161 * @return Capacity in bytes
162 */
163 [[nodiscard]] size_t Capacity() const noexcept { return capacity_; }
164
165 /**
166 * @brief Gets the current offset (amount of memory used).
167 * @return Current offset in bytes
168 */
169 [[nodiscard]] size_t CurrentOffset() const noexcept { return offset_.load(std::memory_order_relaxed); }
170
171 /**
172 * @brief Gets the amount of free space remaining.
173 * @return Free space in bytes
174 */
175 [[nodiscard]] size_t FreeSpace() const noexcept;
176
177private:
178 void* buffer_ = nullptr; ///< Underlying memory buffer
179 size_t capacity_ = 0; ///< Total capacity in bytes
180 std::atomic<size_t> offset_{0}; ///< Current allocation offset
181 std::atomic<size_t> peak_offset_{0}; ///< Peak offset reached
182 std::atomic<size_t> allocation_count_{0}; ///< Total number of allocations made
183 std::atomic<size_t> alignment_waste_{0}; ///< Total bytes wasted due to alignment
184};
185
186inline FrameAllocator::FrameAllocator(size_t capacity) : capacity_(capacity) {
187 HELIOS_ASSERT(capacity > 0, "Failed to construct FrameAllocator: capacity must be greater than 0!");
188
189 // Allocate aligned buffer
190 buffer_ = AlignedAlloc(kDefaultAlignment, capacity_);
191 HELIOS_VERIFY(buffer_ != nullptr, "Failed to construct FrameAllocator: Allocation of buffer failed!");
192}
193
195 : buffer_(other.buffer_),
196 capacity_(other.capacity_),
197 offset_(other.offset_.load(std::memory_order_acquire)),
198 peak_offset_(other.peak_offset_.load(std::memory_order_acquire)),
199 allocation_count_(other.allocation_count_.load(std::memory_order_acquire)),
200 alignment_waste_(other.alignment_waste_.load(std::memory_order_acquire)) {
201 other.buffer_ = nullptr;
202 other.capacity_ = 0;
203 other.offset_.store(0, std::memory_order_release);
204 other.peak_offset_.store(0, std::memory_order_release);
205 other.allocation_count_.store(0, std::memory_order_release);
206 other.alignment_waste_.store(0, std::memory_order_release);
207}
208
210 if (buffer_ != nullptr) {
211 AlignedFree(buffer_);
212 }
213}
214
216 if (this == &other) [[unlikely]] {
217 return *this;
218 }
219
220 // Free current buffer
221 if (buffer_ != nullptr) {
222 AlignedFree(buffer_);
223 }
224
225 // Move from other
226 buffer_ = other.buffer_;
227 capacity_ = other.capacity_;
228 offset_.store(other.offset_.load(std::memory_order_acquire), std::memory_order_release);
229 peak_offset_.store(other.peak_offset_.load(std::memory_order_acquire), std::memory_order_release);
230 allocation_count_.store(other.allocation_count_.load(std::memory_order_acquire), std::memory_order_release);
231 alignment_waste_.store(other.alignment_waste_.load(std::memory_order_acquire), std::memory_order_release);
232
233 // Reset other
234 other.buffer_ = nullptr;
235 other.capacity_ = 0;
236 other.offset_.store(0, std::memory_order_release);
237 other.peak_offset_.store(0, std::memory_order_release);
238 other.allocation_count_.store(0, std::memory_order_release);
239 other.alignment_waste_.store(0, std::memory_order_release);
240
241 return *this;
242}
243
244inline AllocationResult FrameAllocator::Allocate(size_t size, size_t alignment) noexcept {
245 HELIOS_ASSERT(IsPowerOfTwo(alignment), "Failed to allocate memory: alignment must be power of 2, got '{}'!",
246 alignment);
247 HELIOS_ASSERT(alignment >= kMinAlignment, "Failed to allocate memory: alignment must be at least '{}', got '{}'!",
248 kMinAlignment, alignment);
249
250 if (size == 0) [[unlikely]] {
251 return {.ptr = nullptr, .allocated_size = 0};
252 }
253
254 // Atomically allocate using compare-and-swap
255 size_t current_offset = offset_.load(std::memory_order_acquire);
256 size_t aligned_offset = 0;
257 size_t new_offset = 0;
258 size_t padding = 0;
259
260 do {
261 // Calculate aligned offset
262 auto* current_ptr = static_cast<uint8_t*>(buffer_) + current_offset;
263 padding = CalculatePadding(current_ptr, alignment);
264 aligned_offset = current_offset + padding;
265
266 // Check if we have enough space
267 if (aligned_offset + size > capacity_) {
268 return {.ptr = nullptr, .allocated_size = 0};
269 }
270
271 new_offset = aligned_offset + size;
272
273 // Try to atomically update offset
274 } while (
275 !offset_.compare_exchange_weak(current_offset, new_offset, std::memory_order_release, std::memory_order_acquire));
276
277 // Update stats (these don't need to be perfectly accurate)
278 allocation_count_.fetch_add(1, std::memory_order_relaxed);
279 alignment_waste_.fetch_add(padding, std::memory_order_relaxed);
280
281 // Update peak offset
282 size_t current_peak = peak_offset_.load(std::memory_order_acquire);
283 while (new_offset > current_peak) {
284 if (peak_offset_.compare_exchange_weak(current_peak, new_offset, std::memory_order_release,
285 std::memory_order_acquire)) {
286 break;
287 }
288 }
289
290 void* result = static_cast<uint8_t*>(buffer_) + aligned_offset;
291 return {.ptr = result, .allocated_size = size};
292}
293
294inline void FrameAllocator::Reset() noexcept {
295 offset_.store(0, std::memory_order_release);
296 alignment_waste_.store(0, std::memory_order_release);
297 allocation_count_.store(0, std::memory_order_release);
298}
299
300inline AllocatorStats FrameAllocator::Stats() const noexcept {
301 const size_t current_offset = offset_.load(std::memory_order_relaxed);
302 const size_t peak = peak_offset_.load(std::memory_order_relaxed);
303 const size_t alloc_count = allocation_count_.load(std::memory_order_relaxed);
304 const size_t waste = alignment_waste_.load(std::memory_order_relaxed);
305
306 return {
307 .total_allocated = current_offset,
308 .total_freed = 0,
309 .peak_usage = peak,
310 .allocation_count = alloc_count,
311 .total_allocations = alloc_count,
312 .total_deallocations = 0,
313 .alignment_waste = waste,
314 };
315}
316
317inline size_t FrameAllocator::FreeSpace() const noexcept {
318 const size_t current = offset_.load(std::memory_order_relaxed);
319 return current < capacity_ ? capacity_ - current : 0;
320}
321
322template <typename T>
323inline T* FrameAllocator::Allocate() noexcept {
324 constexpr size_t size = sizeof(T);
325 constexpr size_t alignment = std::max(alignof(T), kMinAlignment);
326 auto result = Allocate(size, alignment);
327 return static_cast<T*>(result.ptr);
328}
329
330template <typename T>
331inline T* FrameAllocator::Allocate(size_t count) noexcept {
332 if (count == 0) [[unlikely]] {
333 return nullptr;
334 }
335 constexpr size_t alignment = std::max(alignof(T), kMinAlignment);
336 const size_t size = sizeof(T) * count;
337 auto result = Allocate(size, alignment);
338 return static_cast<T*>(result.ptr);
339}
340
341template <typename T, typename... Args>
342 requires std::constructible_from<T, Args...>
343inline T* FrameAllocator::AllocateAndConstruct(Args&&... args) noexcept(std::is_nothrow_constructible_v<T, Args...>) {
344 T* ptr = Allocate<T>();
345 if (ptr != nullptr) [[likely]] {
346 std::construct_at(ptr, std::forward<Args>(args)...);
347 }
348 return ptr;
349}
350
351template <typename T>
352 requires std::default_initializable<T>
353inline T* FrameAllocator::AllocateAndConstructArray(size_t count) noexcept(std::is_nothrow_default_constructible_v<T>) {
354 T* ptr = Allocate<T>(count);
355 if (ptr != nullptr) [[likely]] {
356 for (size_t i = 0; i < count; ++i) {
357 std::construct_at(ptr + i);
358 }
359 }
360 return ptr;
361}
362
363} // namespace helios::memory
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:140
#define HELIOS_VERIFY(condition,...)
Verify macro that always checks the condition.
Definition assert.hpp:196
Linear allocator that clears every frame.
size_t Capacity() const noexcept
Gets the total capacity of the allocator.
FrameAllocator(size_t capacity)
Constructs a frame allocator with specified capacity.
void Reset() noexcept
Resets the allocator, freeing all allocations.
T * AllocateAndConstructArray(size_t count) noexcept(std::is_nothrow_default_constructible_v< T >)
FrameAllocator & operator=(const FrameAllocator &)=delete
AllocatorStats Stats() const noexcept
Gets current allocator statistics.
bool Empty() const noexcept
Checks if the allocator is empty.
size_t FreeSpace() const noexcept
Gets the amount of free space remaining.
size_t CurrentOffset() const noexcept
Gets the current offset (amount of memory used).
T * AllocateAndConstruct(Args &&... args) noexcept(std::is_nothrow_constructible_v< T, Args... >)
bool Full() const noexcept
Checks if the allocator is full.
FrameAllocator(const FrameAllocator &)=delete
size_t CalculatePadding(const void *ptr, size_t alignment) noexcept
Calculate padding needed for alignment.
constexpr size_t kDefaultAlignment
Default alignment for allocations (cache line size for most modern CPUs).
void * AlignedAlloc(size_t alignment, size_t size)
Allocate memory with the specified alignment.
Definition common.hpp:30
constexpr bool IsPowerOfTwo(size_t size) noexcept
Helper function to check if a size is a power of 2.
constexpr size_t kMinAlignment
Minimum alignment for any allocation.
void AlignedFree(void *ptr)
Free memory allocated with AlignedAlloc.
Definition common.hpp:53
constexpr T * Allocate(Alloc &allocator) noexcept
STL namespace.
Result type for allocation operations.
Statistics for tracking allocator usage.