File size: 9,757 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vector>
#include <new>
#include <utility>
#include <atomic>
#include <string>
#include <map>

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<uint64_t> allocations;
    static std::atomic<uint64_t> 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<uintptr_t>(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 <typename T> 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<uint8_t> 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<void**>(free_);
        in_use_++;
        return p;
    }
    void free(void* p) {
        if (!p) return;
        *static_cast<void**>(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<uint8_t*>(::operator new(block_ * per_page_));
        pages_.push_back(page);
        for (size_t i = per_page_; i-- > 0;) {
            void* b = page + i * block_;
            *static_cast<void**>(b) = free_;
            free_ = b;
        }
    }
    size_t block_, per_page_;
    std::vector<uint8_t*> pages_;
    void* free_ = nullptr;
    size_t in_use_ = 0;
};

/// Typed object pool on top of PoolAllocator. Constructs in place; destroy() runs the destructor.
template <typename T>
class ObjectPool {
public:
    explicit ObjectPool(size_t per_page = 256) : pool_(sizeof(T), per_page) {}
    template <typename... Args> T* create(Args&&... args) { return new (pool_.allocate()) T(std::forward<Args>(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 <typename T>
class PoolStdAllocator {
public:
    using value_type = T;
    PoolStdAllocator() noexcept = default;
    template <typename U> PoolStdAllocator(const PoolStdAllocator<U>&) noexcept {}
    T* allocate(size_t n) {
        if (n == 1) return static_cast<T*>(pool().allocate());
        return static_cast<T*>(::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 <typename U> bool operator==(const PoolStdAllocator<U>&) const noexcept { return true; }
    template <typename U> bool operator!=(const PoolStdAllocator<U>&) 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<uint8_t*>(::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<std::string, size_t> by_category_;
    std::map<uint8_t*, std::pair<size_t, std::string>> 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<uint32_t, uint32_t>& used_ranges() const { return used_; }
    void restore(const std::map<uint32_t, uint32_t>& 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<uint32_t, uint32_t> free_, used_;
    uint32_t base_ = 0, end_ = 0;
};

}  // namespace unify