#pragma once #include #include #include "fl/inplacenew.h" #include "fl/memfill.h" #include "fl/type_traits.h" #include "fl/unused.h" #include "fl/bit_cast.h" #include "fl/stdint.h" #include "fl/bitset.h" #ifndef FASTLED_DEFAULT_SLAB_SIZE #define FASTLED_DEFAULT_SLAB_SIZE 8 #endif namespace fl { // Test hooks for malloc/free operations #if defined(FASTLED_TESTING) // Interface class for malloc/free test hooks class MallocFreeHook { public: virtual ~MallocFreeHook() = default; virtual void onMalloc(void* ptr, fl::size size) = 0; virtual void onFree(void* ptr) = 0; }; // Set test hooks for malloc and free operations void SetMallocFreeHook(MallocFreeHook* hook); // Clear test hooks (set to nullptr) void ClearMallocFreeHook(); #endif void SetPSRamAllocator(void *(*alloc)(fl::size), void (*free)(void *)); void *PSRamAllocate(fl::size size, bool zero = true); void PSRamDeallocate(void *ptr); void* Malloc(fl::size size); void Free(void *ptr); template class PSRamAllocator { public: static T *Alloc(fl::size n) { void *ptr = PSRamAllocate(sizeof(T) * n, true); return fl::bit_cast_ptr(ptr); } static void Free(T *p) { if (p == nullptr) { return; } PSRamDeallocate(p); } }; // std compatible allocator. template class allocator { public: // Type definitions required by STL using value_type = T; using pointer = T*; using const_pointer = const T*; using reference = T&; using const_reference = const T&; using size_type = fl::size; using difference_type = ptrdiff_t; // Rebind allocator to type U template struct rebind { using other = allocator; }; // Default constructor allocator() noexcept {} // Copy constructor template allocator(const allocator&) noexcept {} // Destructor ~allocator() noexcept {} // Use this to allocate large blocks of memory for T. // This is useful for large arrays or objects that need to be allocated // in a single block. T* allocate(fl::size n) { if (n == 0) { return nullptr; // Handle zero allocation } fl::size size = sizeof(T) * n; void *ptr = Malloc(size); if (ptr == nullptr) { return nullptr; // Handle allocation failure } fl::memfill(ptr, 0, sizeof(T) * n); // Zero-initialize the memory return static_cast(ptr); } void deallocate(T* p, fl::size n) { FASTLED_UNUSED(n); if (p == nullptr) { return; // Handle null pointer } Free(p); // Free the allocated memory } // Construct an object at the specified address template void construct(U* p, Args&&... args) { if (p == nullptr) return; new(static_cast(p)) U(fl::forward(args)...); } // Destroy an object at the specified address template void destroy(U* p) { if (p == nullptr) return; p->~U(); } }; template class allocator_psram { public: // Type definitions required by STL using value_type = T; using pointer = T*; using const_pointer = const T*; using reference = T&; using const_reference = const T&; using size_type = fl::size; using difference_type = ptrdiff_t; // Rebind allocator to type U template struct rebind { using other = allocator_psram; }; // Default constructor allocator_psram() noexcept {} // Copy constructor template allocator_psram(const allocator_psram&) noexcept {} // Destructor ~allocator_psram() noexcept {} // Allocate memory for n objects of type T T* allocate(fl::size n) { return PSRamAllocator::Alloc(n); } // Deallocate memory for n objects of type T void deallocate(T* p, fl::size n) { PSRamAllocator::Free(p); FASTLED_UNUSED(n); } // Construct an object at the specified address template void construct(U* p, Args&&... args) { if (p == nullptr) return; new(static_cast(p)) U(fl::forward(args)...); } // Destroy an object at the specified address template void destroy(U* p) { if (p == nullptr) return; p->~U(); } }; // Slab allocator for fixed-size objects // Optimized for frequent allocation/deallocation of objects of the same size // Uses pre-allocated memory slabs with free lists to reduce fragmentation template class SlabAllocator { private: static constexpr fl::size SLAB_BLOCK_SIZE = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*); static constexpr fl::size BLOCKS_PER_SLAB = SLAB_SIZE; static constexpr fl::size SLAB_MEMORY_SIZE = SLAB_BLOCK_SIZE * BLOCKS_PER_SLAB; struct Slab { Slab* next; u8* memory; fl::size allocated_count; fl::bitset_fixed allocated_blocks; // Track which blocks are allocated Slab() : next(nullptr), memory(nullptr), allocated_count(0) {} ~Slab() { if (memory) { free(memory); } } }; Slab* slabs_; fl::size total_allocated_; fl::size total_deallocated_; Slab* createSlab() { Slab* slab = static_cast(malloc(sizeof(Slab))); if (!slab) { return nullptr; } // Use placement new to properly initialize the Slab new(slab) Slab(); slab->memory = static_cast(malloc(SLAB_MEMORY_SIZE)); if (!slab->memory) { slab->~Slab(); free(slab); return nullptr; } // Initialize all blocks in the slab as free slab->allocated_blocks.reset(); // All blocks start as free // Add slab to the slab list slab->next = slabs_; slabs_ = slab; return slab; } void* allocateFromSlab(fl::size n = 1) { // Try to find n contiguous free blocks in existing slabs for (Slab* slab = slabs_; slab; slab = slab->next) { void* ptr = findContiguousBlocks(slab, n); if (ptr) { return ptr; } } // No contiguous blocks found, create new slab if n fits if (n <= BLOCKS_PER_SLAB) { if (!createSlab()) { return nullptr; // Out of memory } // Try again with the new slab return findContiguousBlocks(slabs_, n); } // Request too large for slab, fall back to malloc return nullptr; } void* findContiguousBlocks(Slab* slab, fl::size n) { // Check if allocation is too large for this slab if (n > BLOCKS_PER_SLAB) { return nullptr; } // Use bitset's find_run to find n contiguous free blocks (false = free) fl::i32 start = slab->allocated_blocks.find_run(false, static_cast(n)); if (start >= 0) { // Mark blocks as allocated for (fl::size i = 0; i < n; ++i) { slab->allocated_blocks.set(static_cast(start + i), true); } slab->allocated_count += n; total_allocated_ += n; // Return pointer to the first block return slab->memory + static_cast(start) * SLAB_BLOCK_SIZE; } return nullptr; } void deallocateToSlab(void* ptr, fl::size n = 1) { if (!ptr) { return; } // Find which slab this block belongs to for (Slab* slab = slabs_; slab; slab = slab->next) { u8* slab_start = slab->memory; u8* slab_end = slab_start + SLAB_MEMORY_SIZE; u8* block_ptr = fl::bit_cast_ptr(ptr); if (block_ptr >= slab_start && block_ptr < slab_end) { fl::size block_index = (block_ptr - slab_start) / SLAB_BLOCK_SIZE; // Mark blocks as free in the bitset for (fl::size i = 0; i < n; ++i) { if (block_index + i < BLOCKS_PER_SLAB) { slab->allocated_blocks.set(block_index + i, false); } } slab->allocated_count -= n; total_deallocated_ += n; break; } } } public: // Constructor SlabAllocator() : slabs_(nullptr), total_allocated_(0), total_deallocated_(0) {} // Destructor ~SlabAllocator() { cleanup(); } // Non-copyable SlabAllocator(const SlabAllocator&) = delete; SlabAllocator& operator=(const SlabAllocator&) = delete; // Movable SlabAllocator(SlabAllocator&& other) noexcept : slabs_(other.slabs_), total_allocated_(other.total_allocated_), total_deallocated_(other.total_deallocated_) { other.slabs_ = nullptr; other.total_allocated_ = 0; other.total_deallocated_ = 0; } SlabAllocator& operator=(SlabAllocator&& other) noexcept { if (this != &other) { cleanup(); slabs_ = other.slabs_; total_allocated_ = other.total_allocated_; total_deallocated_ = other.total_deallocated_; other.slabs_ = nullptr; other.total_allocated_ = 0; other.total_deallocated_ = 0; } return *this; } T* allocate(fl::size n = 1) { if (n == 0) { return nullptr; } // Try to allocate from slab first void* ptr = allocateFromSlab(n); if (ptr) { fl::memfill(ptr, 0, sizeof(T) * n); return static_cast(ptr); } // Fall back to regular malloc for large allocations ptr = malloc(sizeof(T) * n); if (ptr) { fl::memfill(ptr, 0, sizeof(T) * n); } return static_cast(ptr); } void deallocate(T* ptr, fl::size n = 1) { if (!ptr) { return; } // Try to deallocate from slab first bool found_in_slab = false; for (Slab* slab = slabs_; slab; slab = slab->next) { u8* slab_start = slab->memory; u8* slab_end = slab_start + SLAB_MEMORY_SIZE; u8* block_ptr = fl::bit_cast_ptr(static_cast(ptr)); if (block_ptr >= slab_start && block_ptr < slab_end) { deallocateToSlab(ptr, n); found_in_slab = true; break; } } if (!found_in_slab) { // This was allocated with regular malloc free(ptr); } } // Get allocation statistics fl::size getTotalAllocated() const { return total_allocated_; } fl::size getTotalDeallocated() const { return total_deallocated_; } fl::size getActiveAllocations() const { return total_allocated_ - total_deallocated_; } // Get number of slabs fl::size getSlabCount() const { fl::size count = 0; for (Slab* slab = slabs_; slab; slab = slab->next) { ++count; } return count; } // Cleanup all slabs void cleanup() { while (slabs_) { Slab* next = slabs_->next; slabs_->~Slab(); free(slabs_); slabs_ = next; } total_allocated_ = 0; total_deallocated_ = 0; } }; // STL-compatible slab allocator template class allocator_slab { public: // Type definitions required by STL using value_type = T; using pointer = T*; using const_pointer = const T*; using reference = T&; using const_reference = const T&; using size_type = fl::size; using difference_type = ptrdiff_t; // Rebind allocator to type U template struct rebind { using other = typename fl::conditional< fl::is_same::value, allocator_slab, allocator_slab >::type; }; // Default constructor allocator_slab() noexcept {} // Copy constructor allocator_slab(const allocator_slab& other) noexcept { FASTLED_UNUSED(other); } // Copy assignment allocator_slab& operator=(const allocator_slab& other) noexcept { FASTLED_UNUSED(other); return *this; } // Template copy constructor template allocator_slab(const allocator_slab& other) noexcept { FASTLED_UNUSED(other); } // Destructor ~allocator_slab() noexcept {} private: // Get the shared static allocator instance static SlabAllocator& get_allocator() { static SlabAllocator allocator; return allocator; } public: // Allocate memory for n objects of type T T* allocate(fl::size n) { // Use a static allocator instance per type/size combination SlabAllocator& allocator = get_allocator(); return allocator.allocate(n); } // Deallocate memory for n objects of type T void deallocate(T* p, fl::size n) { // Use the same static allocator instance SlabAllocator& allocator = get_allocator(); allocator.deallocate(p, n); } // Construct an object at the specified address template void construct(U* p, Args&&... args) { if (p == nullptr) return; new(static_cast(p)) U(fl::forward(args)...); } // Destroy an object at the specified address template void destroy(U* p) { if (p == nullptr) return; p->~U(); } // Cleanup method to clean up the static slab allocator void cleanup() { // Access the same static allocator instance and clean it up static SlabAllocator allocator; allocator.cleanup(); } // Equality comparison bool operator==(const allocator_slab& other) const noexcept { FASTLED_UNUSED(other); return true; // All instances are equivalent } bool operator!=(const allocator_slab& other) const noexcept { return !(*this == other); } }; // Inlined allocator that stores the first N elements inline // Falls back to the base allocator for additional elements template > class allocator_inlined { private: // Inlined storage block struct InlinedStorage { alignas(T) u8 data[N * sizeof(T)]; InlinedStorage() { fl::memfill(data, 0, sizeof(data)); } }; InlinedStorage m_inlined_storage; BaseAllocator m_base_allocator; fl::size m_inlined_used = 0; fl::bitset_fixed m_free_bits; // Track free slots for inlined memory only fl::size m_active_allocations = 0; // Track current active allocations public: // Type definitions required by STL using value_type = T; using pointer = T*; using const_pointer = const T*; using reference = T&; using const_reference = const T&; using size_type = fl::size; using difference_type = ptrdiff_t; // Rebind allocator to type U template struct rebind { using other = allocator_inlined::other>; }; // Default constructor allocator_inlined() noexcept = default; // Copy constructor allocator_inlined(const allocator_inlined& other) noexcept { // Copy inlined data m_inlined_used = other.m_inlined_used; for (fl::size i = 0; i < m_inlined_used; ++i) { new (&get_inlined_ptr()[i]) T(other.get_inlined_ptr()[i]); } // Copy free bits m_free_bits = other.m_free_bits; // Note: Heap allocations are not copied, only inlined data // Copy active allocations count m_active_allocations = other.m_active_allocations; } // Copy assignment allocator_inlined& operator=(const allocator_inlined& other) noexcept { if (this != &other) { clear(); // Copy inlined data m_inlined_used = other.m_inlined_used; for (fl::size i = 0; i < m_inlined_used; ++i) { new (&get_inlined_ptr()[i]) T(other.get_inlined_ptr()[i]); } // Copy free bits m_free_bits = other.m_free_bits; // Note: Heap allocations are not copied, only inlined data // Copy active allocations count m_active_allocations = other.m_active_allocations; } return *this; } // Template copy constructor template allocator_inlined(const allocator_inlined::other>& other) noexcept { FASTLED_UNUSED(other); } // Destructor ~allocator_inlined() noexcept { clear(); } // Allocate memory for n objects of type T T* allocate(fl::size n) { if (n == 0) { return nullptr; } // For large allocations (n > 1), use base allocator directly if (n > 1) { T* ptr = m_base_allocator.allocate(n); if (ptr) { m_active_allocations += n; } return ptr; } // For single allocations, first try inlined memory // Find first free inlined slot fl::i32 free_slot = m_free_bits.find_first(false); if (free_slot >= 0 && static_cast(free_slot) < N) { // Mark the inlined slot as used m_free_bits.set(static_cast(free_slot), true); // Update inlined usage tracking if (static_cast(free_slot) + 1 > m_inlined_used) { m_inlined_used = static_cast(free_slot) + 1; } m_active_allocations++; return &get_inlined_ptr()[static_cast(free_slot)]; } // No inlined slots available, use heap allocation T* ptr = m_base_allocator.allocate(1); if (ptr) { m_active_allocations++; } return ptr; } // Deallocate memory for n objects of type T void deallocate(T* p, fl::size n) { if (!p || n == 0) { return; } // Check if this is inlined memory T* inlined_start = get_inlined_ptr(); T* inlined_end = inlined_start + N; if (p >= inlined_start && p < inlined_end) { // This is inlined memory, mark slots as free fl::size slot_index = (p - inlined_start); for (fl::size i = 0; i < n; ++i) { if (slot_index + i < N) { m_free_bits.set(slot_index + i, false); // Mark as free } } m_active_allocations -= n; return; } // Fallback to base allocator for heap allocations m_base_allocator.deallocate(p, n); m_active_allocations -= n; } // Construct an object at the specified address template void construct(U* p, Args&&... args) { if (p == nullptr) return; new(static_cast(p)) U(fl::forward(args)...); } // Destroy an object at the specified address template void destroy(U* p) { if (p == nullptr) return; p->~U(); } // Clear all allocated memory void clear() { // Destroy inlined objects for (fl::size i = 0; i < m_inlined_used; ++i) { get_inlined_ptr()[i].~T(); } m_inlined_used = 0; m_free_bits.reset(); m_active_allocations = 0; // Clean up the base allocator (for SlabAllocator, this clears slabs and free lists) cleanup_base_allocator(); } // Get total allocated size fl::size total_size() const { return m_active_allocations; } // Get inlined capacity fl::size inlined_capacity() const { return N; } // Check if using inlined storage bool is_using_inlined() const { return m_active_allocations == m_inlined_used; } private: T* get_inlined_ptr() { return reinterpret_cast(m_inlined_storage.data); } const T* get_inlined_ptr() const { return reinterpret_cast(m_inlined_storage.data); } // SFINAE helper to detect if base allocator has cleanup() method template static auto has_cleanup_impl(int) -> decltype(fl::declval().cleanup(), fl::true_type{}); template static fl::false_type has_cleanup_impl(...); using has_cleanup = decltype(has_cleanup_impl(0)); // Call cleanup on base allocator if it has the method void cleanup_base_allocator() { cleanup_base_allocator_impl(has_cleanup{}); } void cleanup_base_allocator_impl(fl::true_type) { m_base_allocator.cleanup(); } void cleanup_base_allocator_impl(fl::false_type) { // Base allocator doesn't have cleanup method, do nothing } // Equality comparison bool operator==(const allocator_inlined& other) const noexcept { FASTLED_UNUSED(other); return true; // All instances are equivalent for now } bool operator!=(const allocator_inlined& other) const noexcept { return !(*this == other); } }; // Inlined allocator that uses PSRam for heap allocation template using allocator_inlined_psram = allocator_inlined>; // Inlined allocator that uses slab allocator for heap allocation template using allocator_inlined_slab_psram = allocator_inlined>; template using allocator_inlined_slab = allocator_inlined>; } // namespace fl