Mikemon/src/smol-atlas.cpp

404 lines
10 KiB
C++

// SPDX-License-Identifier: MIT OR Unlicense
// smol-atlas: https://github.com/aras-p/smol-atlas
#include "smol-atlas.h"
#include <assert.h>
#include <stddef.h>
#include <vector>
#include <type_traits>
// "memory pool" that allocates chunks of same size items,
// and maintains a freelist of items for O(1) alloc and free.
template <typename T>
struct smol_pool_t
{
static_assert(std::is_trivially_destructible_v<T>, "smol_pool_t type must be trivially destructible");
union item_t
{
item_t* next;
alignas(alignof(T)) char data[sizeof(T)];
};
struct chunk_t
{
item_t* storage = nullptr;
chunk_t* next = nullptr;
size_t m_size_in_items;
chunk_t(size_t size_in_items) : storage(new item_t[size_in_items]), m_size_in_items(size_in_items)
{
link_all_items(nullptr);
}
~chunk_t() { delete[] storage; }
void link_all_items(item_t* last_item_next_ptr)
{
for (size_t i = 1; i < m_size_in_items; ++i)
storage[i - 1].next = &storage[i];
storage[m_size_in_items - 1].next = last_item_next_ptr;
}
};
size_t m_chunk_size_in_items;
chunk_t* m_cur_chunk = nullptr;
item_t* m_free_list = nullptr;
smol_pool_t(size_t size_in_items)
: m_chunk_size_in_items(size_in_items)
, m_cur_chunk(new chunk_t(size_in_items))
, m_free_list(m_cur_chunk->storage)
{
}
~smol_pool_t()
{
chunk_t* chunk = m_cur_chunk;
while (chunk) {
chunk_t* next = chunk->next;
delete chunk;
chunk = next;
}
}
// makes all items "unused", but keeps the allocated space
void clear()
{
chunk_t* chunk = m_cur_chunk;
while (chunk) {
chunk_t* next = chunk->next;
chunk->link_all_items(next ? next->storage : nullptr);
chunk = next;
}
m_free_list = m_cur_chunk ? m_cur_chunk->storage : nullptr;
}
template <typename... Args> T* alloc(Args &&... args)
{
// create a new chunk if current one is full
if (m_free_list == nullptr) {
chunk_t* new_chunk = new chunk_t(m_chunk_size_in_items);
new_chunk->next = m_cur_chunk;
m_cur_chunk = new_chunk;
m_free_list = m_cur_chunk->storage;
}
// grab item from free list
item_t* item = m_free_list;
m_free_list = item->next;
// construct the object
T* res = reinterpret_cast<T*>(item->data);
new (res) T(std::forward<Args>(args)...);
return res;
}
void free(T* ptr)
{
// add item to the free list
item_t* item = reinterpret_cast<item_t*>(ptr);
item->next = m_free_list;
m_free_list = item;
}
};
static inline int max_i(int a, int b)
{
return a > b ? a : b;
}
struct smol_atlas_item_t
{
explicit smol_atlas_item_t(int x_, int y_, int w_, int h_, int shelf_)
: x(x_), y(y_), width(w_), height(h_), shelf_index(shelf_)
{
}
int x;
int y;
int width;
int height;
int shelf_index;
};
struct smol_free_span_t
{
explicit smol_free_span_t(int x_, int w_) : x(x_), width(w_), next(nullptr) {}
int x;
int width;
smol_free_span_t* next;
};
template<typename T>
struct smol_single_list_t
{
smol_single_list_t(T* h) : m_head(h) { }
void insert(T* prev, T* node)
{
if (prev == nullptr) { // this is first node
node->next = m_head;
m_head = node;
}
else {
node->next = prev->next;
prev->next = node;
}
}
void remove(T* prev, T* node)
{
if (prev)
prev->next = node->next;
else
m_head = node->next;
}
T* m_head = nullptr;
};
struct smol_shelf_t
{
explicit smol_shelf_t(int y, int width, int height, int index, smol_pool_t<smol_free_span_t>& span_pool)
: m_y(y), m_height(height), m_index(index), m_free_spans(span_pool.alloc(0, width))
{
}
bool has_space_for(int width) const
{
smol_free_span_t* it = m_free_spans.m_head;
while (it != nullptr) {
if (width <= it->width)
return true;
it = it->next;
}
return false;
}
smol_atlas_item_t* alloc_item(int w, int h, smol_pool_t<smol_atlas_item_t>& item_pool, smol_pool_t<smol_free_span_t>& span_pool)
{
if (h > m_height)
return nullptr;
// find a suitable free span
smol_free_span_t* it = m_free_spans.m_head;
smol_free_span_t* prev = nullptr;
while (it != nullptr) {
if (it->width >= w) {
break;
}
prev = it;
it = it->next;
}
// no space in this shelf
if (it == nullptr)
return nullptr;
const int x = it->x;
const int rest = it->width - w;
if (rest > 0) {
// there will be still space left in this span, adjust
it->x += w;
it->width -= w;
}
else {
// whole span is taken, remove it
m_free_spans.remove(prev, it);
span_pool.free(it);
}
return item_pool.alloc(x, m_y, w, h, m_index);
}
void add_free_span(int x, int width, smol_pool_t<smol_free_span_t>& span_pool)
{
// insert into free spans list at the right position
smol_free_span_t* free_e = span_pool.alloc(x, width);
smol_free_span_t* it = m_free_spans.m_head;
smol_free_span_t* prev = nullptr;
bool added = false;
while (it != nullptr) {
if (free_e->x < it->x) {
// found right place, insert into the list
m_free_spans.insert(prev, free_e);
added = true;
break;
}
prev = it;
it = it->next;
}
if (!added)
m_free_spans.insert(prev, free_e);
merge_free_spans(prev, free_e, span_pool);
}
void free_item(smol_atlas_item_t* e, smol_pool_t<smol_atlas_item_t>& item_pool, smol_pool_t<smol_free_span_t>& span_pool)
{
assert(e);
assert(e->shelf_index == m_index);
assert(e->y == m_y);
add_free_span(e->x, e->width, span_pool);
item_pool.free(e);
}
void merge_free_spans(smol_free_span_t* prev, smol_free_span_t* span, smol_pool_t<smol_free_span_t>& span_pool)
{
smol_free_span_t* next = span->next;
if (next != nullptr && span->x + span->width == next->x) {
// merge with next
span->width += next->width;
m_free_spans.remove(span, next);
span_pool.free(next);
}
if (prev != nullptr && prev->x + prev->width == span->x) {
// merge with prev
prev->width += span->width;
m_free_spans.remove(prev, span);
span_pool.free(span);
}
}
smol_single_list_t<smol_free_span_t> m_free_spans;
const int m_y;
const int m_height;
const int m_index;
};
struct smol_atlas_t
{
explicit smol_atlas_t(int w, int h)
: m_item_pool(1024), m_span_pool(1024)
{
m_shelves.reserve(8);
m_width = w > 0 ? w : 64;
m_height = h > 0 ? h : 64;
}
~smol_atlas_t()
{
clear();
}
smol_atlas_item_t* pack(int w, int h)
{
// find best shelf
smol_shelf_t* best_shelf = nullptr;
int best_score = 0x7fffffff;
int top_y = 0;
for (auto& shelf : m_shelves) {
const int shelf_h = shelf.m_height;
top_y = max_i(top_y, shelf.m_y + shelf_h);
if (shelf_h < h)
continue; // too short
if (shelf_h == h) { // exact height fit, try to use it
smol_atlas_item_t* res = shelf.alloc_item(w, h, m_item_pool, m_span_pool);
if (res != nullptr)
return res;
}
// otherwise the shelf is too tall, track best one
int score = shelf_h - h;
if (score < best_score && shelf.has_space_for(w)) {
best_score = score;
best_shelf = &shelf;
}
}
if (best_shelf) {
smol_atlas_item_t* res = best_shelf->alloc_item(w, h, m_item_pool, m_span_pool);
if (res != nullptr)
return res;
}
// no shelf with enough space: add a new shelf
if (w <= m_width && h <= m_height - top_y) {
int shelf_index = int(m_shelves.size());
m_shelves.emplace_back(top_y, m_width, h, shelf_index, m_span_pool);
return m_shelves.back().alloc_item(w, h, m_item_pool, m_span_pool);
}
// out of space
return nullptr;
}
void free_item(smol_atlas_item_t* item)
{
if (item == nullptr)
return;
assert(item->shelf_index >= 0 && item->shelf_index < m_shelves.size());
m_shelves[item->shelf_index].free_item(item, m_item_pool, m_span_pool);
}
void clear()
{
m_item_pool.clear();
m_span_pool.clear();
m_shelves.clear();
}
smol_pool_t<smol_atlas_item_t> m_item_pool;
smol_pool_t<smol_free_span_t> m_span_pool;
std::vector<smol_shelf_t> m_shelves;
int m_width;
int m_height;
};
smol_atlas_t* sma_atlas_create(int width, int height)
{
return new smol_atlas_t(width, height);
}
void sma_atlas_destroy(smol_atlas_t* atlas)
{
delete atlas;
}
int sma_atlas_width(const smol_atlas_t* atlas)
{
return atlas->m_width;
}
int sma_atlas_height(const smol_atlas_t* atlas)
{
return atlas->m_height;
}
smol_atlas_item_t* sma_item_add(smol_atlas_t* atlas, int width, int height)
{
return atlas->pack(width, height);
}
void sma_item_remove(smol_atlas_t* atlas, smol_atlas_item_t* item)
{
atlas->free_item(item);
}
void sma_atlas_clear(smol_atlas_t* atlas, int new_width, int new_height)
{
atlas->clear();
if (new_width > 0) atlas->m_width = new_width;
if (new_height > 0) atlas->m_height = new_height;
}
int sma_item_x(const smol_atlas_item_t* item)
{
return item->x;
}
int sma_item_y(const smol_atlas_item_t* item)
{
return item->y;
}
int sma_item_width(const smol_atlas_item_t* item)
{
return item->width;
}
int sma_item_height(const smol_atlas_item_t* item)
{
return item->height;
}