/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_PS_LINEARALLOCATOR
#define INCLUDED_PS_LINEARALLOCATOR
#include "lib/bits.h"
#include "ps/containers/StaticVector.h"
#include
#include
#include
namespace PS::Memory
{
/**
* Linear allocator (also known as an arena allocator or bump allocator) is
* a simple and fast system that allocates memory sequentially from a single
* growing memory buffer. Each allocation only moves a free pointer forward.
* Releasing all allocations happens at the Release call.
*
* It's not thread-safe.
*
* It's intendent to use for allocations with a short lifespan (f.e. function
* scoped).
*
* There is std::pmr::monotonic_buffer_resource but it doesn't provide
* information about the current capacity and doesn't allow to limit the
* capacity. Also avoiding virtual calls for frequent allocations.
*/
class LinearAllocator
{
public:
LinearAllocator(const std::size_t initialSize, const std::size_t maxCapacity)
: m_Buffer{std::make_unique(initialSize)}, m_Capacity{initialSize}, m_MaxCapacity{maxCapacity}
{
ENSURE(initialSize > 0);
}
~LinearAllocator()
{
Release();
}
void* allocate(std::size_t n, std::size_t alignment)
{
m_Size = ROUND_UP(m_Size, alignment);
if (m_Size + n > m_Capacity)
{
m_BuffersToFree.emplace_back(std::move(m_Buffer));
m_Size = 0;
m_Capacity = std::min(std::max(round_up_to_pow2(n), m_Capacity * 2), m_MaxCapacity);
if (n > m_Capacity)
{
throw CapacityExceededException{fmt::format(
"Tried to allocate from LinearAllocator with a size of {} but the capacity is only {}",
n, m_Capacity)};
}
m_Buffer = std::make_unique(m_Capacity);
}
void* ptr{m_Buffer.get() + m_Size};
m_Size += n;
++m_AllocationCount;
return ptr;
}
void deallocate([[maybe_unused]] void* ptr, [[maybe_unused]] std::size_t n)
{
// Do nothing. All allocations will be removed in Release.
ENSURE(m_AllocationCount > 0);
--m_AllocationCount;
}
std::size_t GetSize() const { return m_Size; }
std::size_t GetCapacity() const { return m_Capacity; }
void Release()
{
ENSURE(m_AllocationCount == 0);
m_Size = 0u;
m_BuffersToFree.clear();
}
private:
std::size_t m_Size{0u}, m_Capacity, m_MaxCapacity;
std::unique_ptr m_Buffer;
// We keep track of the allocation count to catch incorrect usages.
std::uint32_t m_AllocationCount{0u};
// initialSize * 2^16 bytes should be enough for all allocations.
PS::StaticVector, 16> m_BuffersToFree;
};
/**
* @see LinearAllocator
*
* Scoped linear allocators ensures that the base allocator is empty.
*
* TODO: currently it gives a very simple tree structure of one root node -
* base allocator and many leaves. We need to implement a tree-like call
* structure. It allows to reuse memory between different "tree" branches.
* While a "node" is locked nobody can't allocate from that allocator.
*/
class ScopedLinearAllocator
{
public:
ScopedLinearAllocator(LinearAllocator& allocator)
: m_Allocator{allocator}
{
ENSURE(m_Allocator.GetSize() == 0);
}
~ScopedLinearAllocator()
{
ENSURE(m_AllocationCount == 0);
m_Allocator.Release();
}
void* allocate(std::size_t n, std::size_t alignment)
{
++m_AllocationCount;
return m_Allocator.allocate(n, alignment);
}
void deallocate(void* ptr, std::size_t n)
{
m_Allocator.deallocate(ptr, n);
ENSURE(m_AllocationCount > 0);
--m_AllocationCount;
}
private:
LinearAllocator& m_Allocator;
std::uint32_t m_AllocationCount{0u};
};
} // namespace PS::Memory
#endif // INCLUDED_PS_LINEARALLOCATOR