#pragma once #include #include #include #include #include #include #include #include #include namespace unify { inline size_t align_up(size_t v, size_t a) { return (v + a - 1) & ~(a - 1); } /// Process-wide count of global operator new calls. Lets tests assert that the /// steady-state simulation tick performs no heap allocation. struct HeapStats { static std::atomic allocations; static std::atomic bytes; }; // --------------------------------------------------------------------------------------- /// Linear per-frame scratch memory. Reset once per frame; allocation is a pointer bump. /// Overflow is a hard failure reported via nullptr + counter, never a silent heap fallback. class FrameAllocator { public: explicit FrameAllocator(size_t capacity) : buffer_(capacity) {} void* allocate(size_t size, size_t align = alignof(std::max_align_t)) { // Align the address, not the offset: the buffer itself is only malloc-aligned. uintptr_t base = reinterpret_cast(buffer_.data()); size_t start = size_t(align_up(base + offset_, align) - base); if (start + size > buffer_.size()) { overflows_++; return nullptr; } offset_ = start + size; if (offset_ > high_water_) high_water_ = offset_; return buffer_.data() + start; } template T* alloc_array(size_t n) { void* p = allocate(sizeof(T) * n, alignof(T)); return p ? new (p) T[n]() : nullptr; } void reset() { offset_ = 0; } size_t used() const { return offset_; } size_t capacity() const { return buffer_.size(); } size_t high_water() const { return high_water_; } uint64_t overflows() const { return overflows_; } private: std::vector buffer_; size_t offset_ = 0, high_water_ = 0; uint64_t overflows_ = 0; }; // --------------------------------------------------------------------------------------- /// Fixed-size block allocator over pre-reserved pages; O(1) alloc/free via intrusive free list. class PoolAllocator { public: PoolAllocator(size_t block_size, size_t blocks_per_page) : block_(align_up(std::max(block_size, sizeof(void*)), alignof(std::max_align_t))), per_page_(blocks_per_page) {} ~PoolAllocator() { for (auto* p : pages_) ::operator delete(p); } PoolAllocator(const PoolAllocator&) = delete; PoolAllocator& operator=(const PoolAllocator&) = delete; void* allocate() { if (!free_) grow(); void* p = free_; free_ = *static_cast(free_); in_use_++; return p; } void free(void* p) { if (!p) return; *static_cast(p) = free_; free_ = p; in_use_--; } size_t in_use() const { return in_use_; } size_t capacity() const { return pages_.size() * per_page_; } size_t block_size() const { return block_; } void reserve_blocks(size_t n) { while (capacity() < n) grow(); } private: void grow() { auto* page = static_cast(::operator new(block_ * per_page_)); pages_.push_back(page); for (size_t i = per_page_; i-- > 0;) { void* b = page + i * block_; *static_cast(b) = free_; free_ = b; } } size_t block_, per_page_; std::vector pages_; void* free_ = nullptr; size_t in_use_ = 0; }; /// Typed object pool on top of PoolAllocator. Constructs in place; destroy() runs the destructor. template class ObjectPool { public: explicit ObjectPool(size_t per_page = 256) : pool_(sizeof(T), per_page) {} template T* create(Args&&... args) { return new (pool_.allocate()) T(std::forward(args)...); } void destroy(T* obj) { if (obj) { obj->~T(); pool_.free(obj); } } size_t live() const { return pool_.in_use(); } void reserve(size_t n) { pool_.reserve_blocks(n); } private: PoolAllocator pool_; }; /// STL allocator over a per-type PoolAllocator. Node-based containers (std::map contacts) /// recycle nodes through the pool instead of hitting the heap on every insert/erase. template class PoolStdAllocator { public: using value_type = T; PoolStdAllocator() noexcept = default; template PoolStdAllocator(const PoolStdAllocator&) noexcept {} T* allocate(size_t n) { if (n == 1) return static_cast(pool().allocate()); return static_cast(::operator new(n * sizeof(T))); } void deallocate(T* p, size_t n) noexcept { if (n == 1) pool().free(p); else ::operator delete(p); } static PoolAllocator& pool() { static PoolAllocator p(sizeof(T), 512); return p; } template bool operator==(const PoolStdAllocator&) const noexcept { return true; } template bool operator!=(const PoolStdAllocator&) const noexcept { return false; } }; // --------------------------------------------------------------------------------------- /// Accounts asset memory per category against a budget. Asset loaders allocate through it /// so leaks after scene unload are observable (bytes return to baseline). class AssetAllocator { public: explicit AssetAllocator(size_t budget) : budget_(budget) {} uint8_t* allocate(size_t bytes, const char* category) { if (used_ + bytes > budget_) { failures_++; return nullptr; } auto* p = static_cast(::operator new(bytes, std::nothrow)); if (!p) { failures_++; return nullptr; } used_ += bytes; by_category_[category] += bytes; sizes_[p] = {bytes, category}; return p; } void free(uint8_t* p) { auto it = sizes_.find(p); if (it == sizes_.end()) return; used_ -= it->second.first; by_category_[it->second.second] -= it->second.first; sizes_.erase(it); ::operator delete(p); } size_t used() const { return used_; } size_t budget() const { return budget_; } uint64_t failures() const { return failures_; } size_t used_by(const std::string& category) const { auto it = by_category_.find(category); return it == by_category_.end() ? 0 : it->second; } size_t live_allocations() const { return sizes_.size(); } private: size_t budget_, used_ = 0; uint64_t failures_ = 0; std::map by_category_; std::map> sizes_; }; // --------------------------------------------------------------------------------------- /// Sub-allocates ranges of an externally owned linear memory (e.g. a WASM instance's memory). /// First-fit over an ordered free list with coalescing. Offsets, not pointers, are returned, /// because the backing memory may grow and move. class BufferAllocator { public: explicit BufferAllocator(uint32_t base = 0, uint32_t size = 0) { reset(base, size); } void reset(uint32_t base, uint32_t size) { free_.clear(); used_.clear(); if (size) free_[base] = size; base_ = base; end_ = base + size; } /// Extends the managed range (after the backing memory grew). void extend(uint32_t new_end) { if (new_end <= end_) return; insert_free(end_, new_end - end_); end_ = new_end; } static constexpr uint32_t kFail = 0xFFFFFFFFu; uint32_t allocate(uint32_t size, uint32_t align = 16) { if (size == 0) size = 1; for (auto it = free_.begin(); it != free_.end(); ++it) { uint32_t start = uint32_t(align_up(it->first, align)); uint32_t pad = start - it->first; if (it->second < pad + size) continue; uint32_t block_start = it->first, block_size = it->second; free_.erase(it); if (pad) free_[block_start] = pad; uint32_t tail = block_size - pad - size; if (tail) free_[start + size] = tail; used_[start] = size; return start; } return kFail; } bool free(uint32_t offset) { auto it = used_.find(offset); if (it == used_.end()) return false; uint32_t size = it->second; used_.erase(it); insert_free(offset, size); return true; } uint32_t size_of(uint32_t offset) const { auto it = used_.find(offset); return it == used_.end() ? 0 : it->second; } size_t live() const { return used_.size(); } uint32_t bytes_used() const { uint32_t n = 0; for (auto& kv : used_) n += kv.second; return n; } uint32_t largest_free() const { uint32_t m = 0; for (auto& kv : free_) m = std::max(m, kv.second); return m; } const std::map& used_ranges() const { return used_; } void restore(const std::map& used, uint32_t base, uint32_t end) { reset(base, end - base); free_.clear(); used_ = used; uint32_t cursor = base; for (auto& kv : used_) { if (kv.first > cursor) free_[cursor] = kv.first - cursor; cursor = kv.first + kv.second; } if (cursor < end) free_[cursor] = end - cursor; } private: void insert_free(uint32_t offset, uint32_t size) { auto next = free_.lower_bound(offset); if (next != free_.end() && offset + size == next->first) { size += next->second; next = free_.erase(next); } if (next != free_.begin()) { auto prev = std::prev(next); if (prev->first + prev->second == offset) { prev->second += size; return; } } free_[offset] = size; } std::map free_, used_; uint32_t base_ = 0, end_ = 0; }; } // namespace unify