Compare commits

..

30 Commits

Author SHA1 Message Date
1153f552d0 Heap Allocator now commits and releases pages dynamically.
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
Implemented Heap::free
Heap::reset() now clears m_usedSize
2026-09-28 00:14:45 -07:00
713fa239f5 Added Region::maybeGrow and Region::maybeShrink helper functions to be shared by multiple Allocators 2026-09-28 00:13:14 -07:00
9fe6784da1 Arena Allocator - Fix Arena::getMaxSize() to return maximum size of the Region rather than the size of the committed pages. 2026-09-28 00:11:50 -07:00
a9bd73ef6a Arena Allocator - Fix 32-bit limitation 2026-09-28 00:10:18 -07:00
49c4ae482e Implement Heap::reset()
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-27 22:17:59 -07:00
07bb4a4628 WIP Block allocator implementation
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-20 16:22:23 -07:00
513db7130b Add tests for Heap allocator
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-20 14:19:07 -07:00
f6bcb3adbd Add .vs to gitignore
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-20 14:18:20 -07:00
033bf5d6ef Add tests for Heap allocator
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
Add Heap::getMaxSize()
Add Heap::getUsed()
Add minSize argument to Heap::init()
2026-09-15 22:48:03 -07:00
76062cdf03 Rename Arena.ArenaFillAndReset test to Arena.FillAndReset
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-11 00:13:39 -07:00
e089dbf3ac Fix tests on Linux
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-11 00:10:22 -07:00
1cb44e96b1 Add Arena::getMaxSize() and Arena::getUsed(). Make Arena tests discoverable.
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-11 00:08:24 -07:00
a7e97b2461 Add Unit tests for Arena allocator
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-11 00:05:39 -07:00
f3e9c789b9 Fix the Region.OutOfMemory test
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-10 21:37:27 -07:00
f70de19d0d Fix Region.Oversized test
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-10 21:32:57 -07:00
5d4e239c62 Adjust CTest arguments in github workflow
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-10 21:26:09 -07:00
3eb8f04cf7 Fixing test working directory for github actions
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-10 21:23:05 -07:00
189bdb9468 Fixing test working directory for github actions
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-10 21:15:21 -07:00
823c28d38d Fix large allocation tests for mimir::Region
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-09 23:32:05 -07:00
90513a431d Add more unit tests for mimir::Region
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-09 23:16:01 -07:00
1c8e5ba5e5 Add unit tests for mimir::Region
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-09-09 22:42:07 -07:00
35b48891ab WIP Two-Level Segregated Fit (TLSF) implementation
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-08-18 01:40:00 -07:00
0aa9542afd WIP Two-Level Segregated Fit (TLSF) implementation
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-08-18 00:49:30 -07:00
324fefbcfc WIP Two-Level Segregated Fit (TLSF) implementation
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-08-17 23:47:22 -07:00
69b8fa4c72 WIP Two-Level Segregated Fit (TLSF) implementation
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-08-15 19:44:47 -07:00
ae7269e6fd Comments, adding Region to Heap
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-08-15 16:07:00 -07:00
e358b02bea Implemented Region. Arena allocator now uses Region for OS specific allocation and page management.
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-08-15 15:50:32 -07:00
3a6711a942 Added more Heap allocator stub functions.
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
Moved platform api includes into mimir_impl.h
2026-08-15 15:25:50 -07:00
d2582025dc Added stubs for Heap allocator
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2026-08-15 15:17:01 -07:00
3e2b84411c Fixed arena allocator aligned allocation functions
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, macos-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
Moved arena allocator initialization to an explicit init() function to avoid performing this test on each allocation.
2026-08-15 15:10:48 -07:00
20 changed files with 1221 additions and 132 deletions

View File

@@ -76,7 +76,8 @@ jobs:
run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }}
- name: Test - name: Test
working-directory: ${{ steps.strings.outputs.build-output-dir }} working-directory: ${{ steps.strings.outputs.build-output-dir }}/tests
# Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator).
# See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
run: ctest --build-config ${{ matrix.build_type }} run: ctest --output-on-failure --no-tests=error --build-config ${{ matrix.build_type }}

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.vs
build build

View File

@@ -9,13 +9,17 @@ set(PUBLIC_HEADERS
include/mimir.h include/mimir.h
include/arena.h include/arena.h
include/block.h include/block.h
include/heap.h
include/region.h
include/util.h include/util.h
) )
set(SRCS set(SRCS
src/arena.cpp src/arena.cpp
src/block.cpp src/block.cpp
src/heap.cpp
src/mimir.cpp src/mimir.cpp
src/region.cpp
src/util.cpp src/util.cpp
) )
@@ -37,3 +41,5 @@ install(
PUBLIC_HEADER PUBLIC_HEADER
DESTINATION include/mimir DESTINATION include/mimir
) )
add_subdirectory(tests)

View File

@@ -31,29 +31,47 @@
#pragma once #pragma once
#include <string>
#include <cstddef> #include <cstddef>
#include "region.h"
namespace mimir { namespace mimir {
// Arena memory allocator, baed by a mimir::Region
class Arena class Arena
{ {
public: public:
Arena(size_t minSize = 1ULL << 24, size_t maxSize = 1ULL << 32); Arena();
~Arena(); ~Arena();
bool init(size_t minSize = 1ULL << 24, size_t maxSize = 1ULL << 32);
// Allocate `size` bytes
std::byte* alloc(size_t size); std::byte* alloc(size_t size);
// Allocate `size` bytes, aligned to 16 bytes and padded to next 16-byte offset.
std::byte* allocA16(size_t size); std::byte* allocA16(size_t size);
// Allocate `size` bytes, aligned to 64 bytes and padded to next 64-byte offset.
std::byte* allocA64(size_t size); std::byte* allocA64(size_t size);
// Reset the arena, potentially also releasing committed pages
void reset(); void reset();
// Get the actual used size.
// This may differ from the sum of allocations due to alignment requirements.
size_t getUsed() const;
// Get the actual maximum size.
// This may be greater than the maxSize passed into init, due to page size alignment.
size_t getMaxSize() const;
private: private:
std::byte* m_data; Region m_region;
size_t m_minSize; size_t m_minSize;
size_t m_maxSize;
size_t m_committedSize;
size_t m_usedSize; size_t m_usedSize;
static const size_t kWatermarkLen = 8; static const size_t kWatermarkLen = 8;
size_t m_watermark[kWatermarkLen]; size_t m_watermark[kWatermarkLen];
}; // class Arena }; // class Arena
} // namespace mimir } // namespace mimir

View File

@@ -35,6 +35,7 @@
#include <stdint.h> #include <stdint.h>
#if defined(_WIN32) || defined(_WIN64) #if defined(_WIN32) || defined(_WIN64)
#define NOMINMAX
#include <Windows.h> #include <Windows.h>
#endif #endif

101
include/heap.h Normal file
View File

@@ -0,0 +1,101 @@
//
// heap.h
// Kraken Engine / Mimir
//
// Copyright 2026 Kearwood Gilbert. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY KEARWOOD GILBERT ''AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KEARWOOD GILBERT OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The views and conclusions contained in the software and documentation are those of the
// authors and should not be interpreted as representing official policies, either expressed
// or implied, of Kearwood Gilbert.
//
#pragma once
#include <cstddef>
#include "region.h"
namespace mimir {
struct TLSFBlock;
// Heap implementation based on Two-Level Segregated Fit (TLSF) memory allocation
class Heap
{
public:
Heap();
~Heap();
bool init(size_t minSize = 1ULL << 24, size_t maxSize = 1ULL << 32);
// Allocate `size` bytes
std::byte* alloc(size_t size);
// Allocate `size` bytes, aligned to 16 bytes and padded to next 16-byte offset.
std::byte* allocA16(size_t size);
// Allocate `size` bytes, aligned to 64 bytes and padded to next 64-byte offset.
std::byte* allocA64(size_t size);
// Free the allocation at `address`
void free(std::byte* address);
// Reset the heap, freeing all memory and potentially releasing comitted pages.
void reset();
// Get the actual used size.
// This may differ from the sum of allocations due to alignment requirements.
size_t getUsed() const;
// Get the actual maximum size.
// This may be greater than the maxSize passed into init, due to page size alignment.
size_t getMaxSize() const;
private:
Region m_region;
size_t m_minSize;
size_t m_usedSize;
// Add a free block to the index
void insertFreeBlock(TLSFBlock* block);
// Remove a free block from the index
void removeFreeBlock(TLSFBlock* block);
// Find a free block that can hold at least size bytes
TLSFBlock* findFreeBlock(size_t size) const;
// Get the usable size of a block
size_t getBlockUsableSize(const TLSFBlock* block) const;
// Get the next block, in physical order
TLSFBlock* getNextPhysBlock(TLSFBlock* block);
// Returns true if block is a free block
bool isBlockFree(const TLSFBlock* block) const;
// Return true if block is the last block, in physical order
bool isBlockLast(const TLSFBlock* block) const;
};
} // namespace mimir

View File

@@ -31,8 +31,10 @@
#pragma once #pragma once
#include "block.h"
#include "arena.h" #include "arena.h"
#include "block.h"
#include "heap.h"
#include "region.h"
#include "util.h" #include "util.h"
namespace mimir { namespace mimir {

62
include/region.h Normal file
View File

@@ -0,0 +1,62 @@
//
// region.h
// Kraken Engine / Mimir
//
// Copyright 2026 Kearwood Gilbert. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY KEARWOOD GILBERT ''AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KEARWOOD GILBERT OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The views and conclusions contained in the software and documentation are those of the
// authors and should not be interpreted as representing official policies, either expressed
// or implied, of Kearwood Gilbert.
//
#pragma once
#include <cstddef>
namespace mimir {
// Dynamically growable memory region with a contiguous address range.
// Provides OS and hardware agnostic reservation and committing of memory pages.
class Region
{
public:
Region();
~Region();
bool init(size_t maxSize = 1ULL << 32);
bool resize(size_t size);
size_t getSize() const;
size_t getMaxSize() const;
std::byte* getAddress() const;
// Helper functions for allocators
bool maybeGrow(size_t minSize);
void maybeShrink(size_t targetSize);
private:
std::byte* m_data;
size_t m_committedSize;
size_t m_maxSize;
};
} // namespace mimir

View File

@@ -32,43 +32,16 @@
#include "../include/mimir.h" #include "../include/mimir.h"
#include "mimir_impl.h" #include "mimir_impl.h"
#if defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif
#if defined(_WIN32) || defined(_WIN64)
#include <Windows.h>
#include <memoryapi.h>
#endif
#include <errno.h>
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
#include <bit> #include <bit>
namespace mimir { namespace mimir {
struct ArenaControlBlock Arena::Arena()
{ : m_minSize(0)
size_t allocated;
size_t comitted;
size_t next;
};
static_assert(sizeof(ArenaControlBlock) < 4096);
Arena::Arena(size_t minSize, size_t maxSize)
: m_minSize(minSize)
, m_maxSize(maxSize)
, m_data(nullptr)
, m_usedSize(0) , m_usedSize(0)
, m_committedSize(0)
{ {
assert(maxSize >= minSize);
for (size_t i = 0; i < kWatermarkLen; i++) { for (size_t i = 0; i < kWatermarkLen; i++) {
m_watermark[i] = 0; m_watermark[i] = 0;
} }
@@ -76,88 +49,63 @@ Arena::Arena(size_t minSize, size_t maxSize)
Arena::~Arena() Arena::~Arena()
{ {
if (m_data) { }
#if defined(_WIN32) || defined(_WIN64)
if (!VirtualFree((void*)m_data, 0, MEM_RELEASE)) { bool Arena::init(size_t minSize, size_t maxSize)
ReportWindowsLastError("VirtualAlloc"); {
} assert(maxSize >= minSize);
#elif defined(__unix__) || defined(__APPLE__) || defined(ANDROID) assert(m_usedSize == 0);
munmap(m_data, KRAKEN_MEM_ROUND_UP_PAGE(m_maxSize));
#else m_minSize = KRAKEN_MEM_ROUND_UP_PAGE(minSize);
static_assert(false, "Not Implemented"); return m_region.init(maxSize);
#endif }
}
// Get the actual used size.
// This may differ from the sum of allocations due to alignment requirements.
size_t Arena::getUsed() const
{
return m_usedSize;
}
// Get the actual maximum size.
// This may be greater than the maxSize passed into init, due to page size alignment.
size_t Arena::getMaxSize() const
{
return m_region.getMaxSize();
} }
std::byte* Arena::alloc(size_t size) std::byte* Arena::alloc(size_t size)
{ {
size_t neededSize = m_usedSize + size; size_t neededSize = m_usedSize + size;
if (!m_region.maybeGrow(std::max(neededSize, m_minSize))) {
if (m_data == nullptr) { return nullptr;
#if defined(_WIN32) || defined(_WIN64)
m_data = (std::byte*)VirtualAlloc(NULL, KRAKEN_MEM_ROUND_UP_PAGE(m_maxSize), MEM_RESERVE, PAGE_NOACCESS);
if (m_data == nullptr) {
ReportWindowsLastError("VirtualAlloc");
return nullptr;
}
#elif defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
m_data = (std::byte*)mmap(NULL, KRAKEN_MEM_ROUND_UP_PAGE(m_maxSize), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (m_data == MAP_FAILED) {
return nullptr;
}
#else
static_assert(false, "Not Implemented");
#endif
} }
if (neededSize > m_committedSize) std::byte* ret = m_region.getAddress() + m_usedSize;
{
size_t newSize = std::bit_ceil(neededSize) << 1;
if (newSize < m_minSize) {
newSize = m_minSize;
}
newSize = KRAKEN_MEM_ROUND_UP_PAGE(newSize);
#if defined(_WIN32) || defined(_WIN64)
if(VirtualAlloc(m_data + m_committedSize, newSize - m_committedSize, MEM_COMMIT, PAGE_READWRITE) == nullptr) {
ReportWindowsLastError("VirtualAlloc");
return nullptr;
}
#elif defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
if (mprotect(m_data + m_committedSize, newSize - m_committedSize, PROT_READ | PROT_WRITE) != 0) {
return nullptr;
}
#else
static_assert(false, "Not Implemented");
#endif
m_committedSize = newSize;
} // if (neededSize > m_committedSize)
std::byte* ret = m_data + m_usedSize;
m_usedSize = neededSize; m_usedSize = neededSize;
return ret; return ret;
} }
std::byte* Arena::allocA16(size_t size) std::byte* Arena::allocA16(size_t size)
{ {
uint32_t nextByte = (m_usedSize + 15) & ~15; size_t nextByte = (m_usedSize + 15) & ~15;
uint32_t roundedSize = (size + 15) & ~15; size_t roundedSize = (size + 15) & ~15;
if (alloc(size) == nullptr) { if (alloc(roundedSize + nextByte - m_usedSize) == nullptr) {
return nullptr; return nullptr;
} }
return m_data + nextByte; return m_region.getAddress() + nextByte;
} }
std::byte* Arena::allocA64(size_t size) std::byte* Arena::allocA64(size_t size)
{ {
uint32_t nextByte = (m_usedSize + 63) & ~63; size_t nextByte = (m_usedSize + 63) & ~63;
uint32_t roundedSize = (size + 63) & ~63; size_t roundedSize = (size + 63) & ~63;
if (alloc(size) == nullptr) { if (alloc(roundedSize + nextByte - m_usedSize) == nullptr) {
return nullptr; return nullptr;
} }
return m_data + nextByte; return m_region.getAddress() + nextByte;
} }
void Arena::reset() void Arena::reset()
@@ -172,33 +120,7 @@ void Arena::reset()
m_watermark[kWatermarkLen - 1] = m_usedSize; m_watermark[kWatermarkLen - 1] = m_usedSize;
size_t targetSize = std::bit_ceil(highWatermark) << 1; size_t targetSize = std::bit_ceil(highWatermark) << 1;
if (targetSize < m_minSize) { m_region.maybeShrink(std::max(m_minSize, targetSize));
targetSize = m_minSize;
}
size_t thresholdSize = targetSize << 1; // The threshold for shrinking is greater than the target to implement hysteresis
if (m_committedSize > thresholdSize) {
size_t newSize = KRAKEN_MEM_ROUND_UP_PAGE(targetSize);
std::byte* rangeStart = m_data + newSize;
size_t rangeLen = m_committedSize - newSize;
#if defined(_WIN32) || defined(_WIN64)
if (!VirtualAlloc(rangeStart, rangeLen, MEM_RESET, PAGE_NOACCESS)) {
ReportWindowsLastError("VirtualAlloc");
} else {
m_committedSize = newSize;
}
#elif defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
if (madvise(rangeStart, rangeLen, MADV_DONTNEED) != 0) {
// TODO - Log Error or debug build assert
}
if (mprotect(rangeStart, rangeLen, PROT_NONE) != 0) {
// TODO - Log Error or debug build assert
}
#else
static_assert(false, "Not Implemented");
#endif
m_committedSize = newSize;
}
m_usedSize = 0; m_usedSize = 0;
} }

View File

@@ -32,16 +32,9 @@
#include "../include/mimir.h" #include "../include/mimir.h"
#include "mimir_impl.h" #include "mimir_impl.h"
#if defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif
#include <errno.h>
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
#include <bit>
#define KRENGINE_MIN_MMAP 32768 #define KRENGINE_MIN_MMAP 32768

417
src/heap.cpp Normal file
View File

@@ -0,0 +1,417 @@
//
// heap.cpp
// Kraken Engine
//
// Copyright 2026 Kearwood Gilbert. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY KEARWOOD GILBERT ''AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KEARWOOD GILBERT OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The views and conclusions contained in the software and documentation are those of the
// authors and should not be interpreted as representing official policies, either expressed
// or implied, of Kearwood Gilbert.
//
#include "../include/mimir.h"
#include "mimir_impl.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <bit>
namespace mimir {
// Two-Level Segregated Fit (TLSF): http://www.gii.upv.es/tlsf/files/papers/ecrts04_tlsf.pdf
const size_t kMinBlockSize = 16;
struct TLSFBlock
{
size_t size; // LSB: T, F: T = Last physical block, F = Free block
TLSFBlock* prevPhys;
TLSFBlock* prevFree;
TLSFBlock* nextFree;
};
static_assert(sizeof(TLSFBlock) == 32);
struct TLSFIndex
{
uint64_t firstLevelFreeBitMap;
uint8_t secondLevelFreeBitMap[60];
TLSFBlock* secondLevelFreeBlocks[60][16];
};
// First Level Ranges:
// 0: 0b00000000010000 ( 1 ~ 16 )
// 1: 0b00000000100000 ( 17 ~ 32 )
// 2: 0b00000001000000 ( 33 ~ 64 )
// 3: 0b00000010000000 ( 65 ~ 128 )
// 4: 0b00000100000000 ( 129 ~ 256 )
// 5: 0b00001000000000 ( 257 ~ 512 )
// 6: 0b00010000000000 ( 513 ~ 1024 )
// 7: 0b00100000000000 ( 1025 ~ 2048 )
//
// bit_width(15): 4 bit_floor(15): 0b000001000
// bit_width(16): 5 bit_floor(16): 0b000010000
// bit_width(31): 5 bit_floor(31): 0b000010000
// bit_width(32): 6 bit_floor(32): 0b000100000
// bit_width(63): 6 bit_floor(63): 0b000100000
// bit_width(64): 7 bit_floor(64): 0b001000000
/*
uint64_t blockSizeToIndex(uint64_t size)
{
// MSB 60 bits are the first level index
// LSB 4 bits is the second level index
// size 0b0000001101011010000
// bit_width 13
// bit_width - 5 8
// size >> 8 0b0000000000000011010
// & 0b1111 0b1010
size_t firstLevelIndex = std::bit_width(size) - 5;
size_t secondLevelIndex = (size >> firstLevelIndex) & 0b1111;
return (firstLevelIndex << 4) | secondLevelIndex;
}
*/
Heap::Heap()
: m_minSize(0)
, m_usedSize(0)
{
}
Heap::~Heap()
{
}
bool Heap::init(size_t minSize, size_t maxSize)
{
assert(maxSize >= minSize);
assert(m_usedSize == 0);
m_minSize = sizeof(TLSFIndex) + 16;
m_minSize = std::max(m_minSize, minSize);
m_minSize = KRAKEN_MEM_ROUND_UP_PAGE(m_minSize);
if (!m_region.init(maxSize)) {
return false;
}
if (!m_region.resize(m_minSize)) {
return false;
}
reset();
return true;
}
// Get the actual maximum size.
// This may be greater than the maxSize passed into init, due to page size alignment.
size_t Heap::getMaxSize() const
{
return m_region.getSize();
}
// Get the actual used size.
// This may differ from the sum of allocations due to alignment requirements.
size_t Heap::getUsed() const
{
return m_usedSize;
}
size_t Heap::getBlockUsableSize(const TLSFBlock* block) const
{
return (block->size & ~0b11) - 16ULL;
}
TLSFBlock* Heap::getNextPhysBlock(TLSFBlock* block)
{
if (isBlockLast(block)) {
return nullptr;
}
return (TLSFBlock*)((std::byte*)block + block->size);
}
// Return true if block is the last block in physical order
bool Heap::isBlockLast(const TLSFBlock* block) const
{
return (block->size & 0b10) == 0b10;
}
// Returns true if block is a free block
bool Heap::isBlockFree(const TLSFBlock* block) const
{
return (block->size & 0b01) == 0b01;
}
void Heap::insertFreeBlock(TLSFBlock* block)
{
// Add the block to the index
TLSFIndex* index = (TLSFIndex*)m_region.getAddress();
size_t usableSize = getBlockUsableSize(block);
size_t firstLevel = std::bit_width(usableSize);
size_t firstLevelIndex = firstLevel - 5;
size_t secondLevelIndex = (usableSize >> firstLevelIndex) & 0b1111ULL;
// Update free bitmaps
index->firstLevelFreeBitMap |= std::bit_floor(usableSize);
index->secondLevelFreeBitMap[firstLevelIndex] |= secondLevelIndex;
TLSFBlock* prevFirstFreeBlock = index->secondLevelFreeBlocks[firstLevelIndex][secondLevelIndex];
index->secondLevelFreeBlocks[firstLevelIndex][secondLevelIndex] = block;
block->prevFree = nullptr;
if (prevFirstFreeBlock) {
block->nextFree = prevFirstFreeBlock;
prevFirstFreeBlock->prevFree = block;
}
// Mark block as free
block->size |= 0b01; // T=1: Last Block, F=1: Free Block
}
void Heap::removeFreeBlock(TLSFBlock* block)
{
// Remove the block from the index
TLSFIndex* index = (TLSFIndex*)m_region.getAddress();
size_t usableSize = getBlockUsableSize(block);
size_t firstLevel = std::bit_width(usableSize);
size_t firstLevelIndex = firstLevel - 5;
size_t secondLevelIndex = (usableSize >> (firstLevelIndex)) & 0b1111ULL;
if (block->prevFree != nullptr) {
// Link the neighboring free blocks together
block->prevFree->nextFree = block->nextFree;
if (block->nextFree) {
block->nextFree->prevFree = block->prevFree;
}
} else {
// This block was the first for this level
index->secondLevelFreeBlocks[firstLevelIndex][secondLevelIndex] = block->nextFree;
if (block->nextFree != nullptr) {
block->nextFree->prevFree = nullptr;
} else {
// We have removed all the free blocks at this level.
// Update second level bitmask.
index->secondLevelFreeBitMap[firstLevelIndex] &= ~(1ULL << secondLevelIndex);
// Check if there are any remaining free blocks within the first level
if (index->secondLevelFreeBitMap[firstLevelIndex] == 0) {
// This was the last one. Clear the first level bit as well.
index->firstLevelFreeBitMap &= ~(1ULL << firstLevel);
}
}
}
// Mark block as non-free
block->size &= ~0b01; // T=1: Last Block, F=1: Free Block
}
// Find a free block that can hold at least size bytes
TLSFBlock* Heap::findFreeBlock(size_t size) const
{
if (size < kMinBlockSize) {
size = kMinBlockSize;
}
TLSFIndex* index = (TLSFIndex*)m_region.getAddress();
// size: 0b0000000000010000 (16)
// bit_ceil(size): 0b0000000000010000
// - 1 ... 0b0000000000001111
// ~ ... 0b1111111111110000
// size: 0b0000001110011101 (925)
// bit_ceil(size): 0b0000010000000000
// - 1 ... 0b0000001111111111
// ~ ... 0b1111110000000000
uint64_t possibleFirstLevels = ~(std::bit_floor(size) - 1);
uint64_t freeFirstLevels = index->firstLevelFreeBitMap & possibleFirstLevels;
uint64_t selectedFirstLevel = std::countr_zero(freeFirstLevels);
if (selectedFirstLevel == 64) {
// No first level with free blocks of sufficient size.
return nullptr;
}
uint64_t firstLevelBufferMinSize = 1ULL << selectedFirstLevel;
uint64_t secondLevelMask = 0b1111ULL;
if (size > firstLevelBufferMinSize) {
uint64_t delta = size - firstLevelBufferMinSize;
secondLevelMask = (delta >> (selectedFirstLevel - 4)) & 0b1111ULL;
}
uint64_t possibleSecondLevels = secondLevelMask & index->secondLevelFreeBitMap[selectedFirstLevel - 4];
uint64_t selectedSecondLevel = std::countr_zero(possibleSecondLevels);
if (selectedSecondLevel == 64) {
// No free blocks available at this first level.
// Select the next highest available first level.
selectedFirstLevel = std::countr_zero(freeFirstLevels & ~(1ULL << selectedFirstLevel));
if (selectedFirstLevel == 64) {
// No other first level with free blocks of sufficient size.
return nullptr;
}
// Any buffer at the second level will fit this allocation. Pick the smallest buffer.
selectedSecondLevel = std::countr_zero(index->secondLevelFreeBitMap[selectedFirstLevel - 4]);
}
// Take the first free block
TLSFBlock* block = index->secondLevelFreeBlocks[selectedFirstLevel - 4][selectedSecondLevel];
return block;
}
// Allocate `size` bytes
std::byte* Heap::alloc(size_t size)
{
if (size < kMinBlockSize) {
size = kMinBlockSize;
}
TLSFBlock* block = findFreeBlock(size);
if (block == nullptr) {
// No free block found
return nullptr;
}
// Ensure that we can commit pages to back the new block
// as well as a trailing free block header
size_t neededCommit = ((std::byte*)block - m_region.getAddress()) + size + 16ULL + 32ULL;
if (!m_region.maybeGrow(std::max(m_minSize, neededCommit))) {
// Out of system memory
return nullptr;
}
removeFreeBlock(block);
size_t usableSize = getBlockUsableSize(block);
if (size - usableSize >= sizeof(TLSFBlock)) {
// Shrink the block to size; allocate a new free block for the remainder
TLSFBlock* freeBlock = (TLSFBlock*)((std::byte*)block + size + 16ULL);
freeBlock->size = size - usableSize;
freeBlock->prevPhys = block;
freeBlock->prevFree = nullptr;
freeBlock->nextFree = nullptr;
if (isBlockLast(block))
{ // LSB: T, F: T = Last physical block, F = Free block
// Block was the last physical block
block->size &= ~0b10;
freeBlock->size &= 0b10;
}
block->size -= freeBlock->size & ~0b11;
insertFreeBlock(freeBlock);
}
m_usedSize += getBlockUsableSize(block);
return (std::byte*)block + 16;
}
// Allocate `size` bytes, aligned to 16 bytes and padded to next 16-byte offset.
std::byte* Heap::allocA16(size_t size)
{
return nullptr; // not implemented
}
// Allocate `size` bytes, aligned to 64 bytes and padded to next 64-byte offset.
std::byte* Heap::allocA64(size_t size)
{
return nullptr; // not implemented
}
// Free the allocation at `address`
void Heap::free(std::byte* address)
{
TLSFBlock* block = (TLSFBlock*)(address - 16ULL);
m_usedSize -= getBlockUsableSize(block);
TLSFBlock* prevPhys = block->prevPhys;
if (prevPhys && isBlockFree(prevPhys)) {
// The previous block in physical order is a free block.
// Remove it from the index
removeFreeBlock(prevPhys);
// Resize it to glob together with the block to free
prevPhys->size += (block->size & ~0b11);
block = prevPhys;
}
// Reset the free block flag and the last block flag
block->size &= ~0b11;
TLSFBlock* nextPhys = getNextPhysBlock(block);
if (nextPhys == nullptr) {
// This is the last block
block->size &= 0b10;
} else if (isBlockFree(nextPhys)) {
// The next block in physical order is a free block.
// remove it from the index
removeFreeBlock(nextPhys);
// Resize free'd block to glob together with the nextPhys block
block->size += (nextPhys->size & ~0b11);
if (isBlockLast(nextPhys)) {
block->size &= 0b10;
}
}
// Add the free'd block to the index
insertFreeBlock(block);
if (isBlockLast(block)) {
// Maybe free pages
size_t neededSize = ((std::byte*)block - m_region.getAddress()) + 32ULL;
m_region.maybeShrink(neededSize);
}
}
// Reset (or initialize) the heap, freeing all memory and potentially releasing comitted pages.
void Heap::reset()
{
// Clear the index, representing no free blocks
TLSFIndex* index = (TLSFIndex*)m_region.getAddress();
memset(index, 0, sizeof(TLSFIndex));
m_usedSize = 0;
// Free comitted pages except minimum required
m_region.resize(m_minSize);
// Start with one free block, filling the entire Region
TLSFBlock* block = (TLSFBlock*)(m_region.getAddress() + sizeof(TLSFIndex));
block->prevPhys = nullptr;
block->nextFree = nullptr;
block->prevFree = nullptr;
block->size = m_region.getMaxSize();
block->size |= 0b10; // T=1: Last Block, F=1: Free Block
insertFreeBlock(block);
}
} // namespace mimir

View File

@@ -39,7 +39,6 @@
#include <fcntl.h> #include <fcntl.h>
#endif #endif
#include <errno.h>
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>

View File

@@ -31,8 +31,17 @@
#pragma once #pragma once
#if defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif
#if defined(_WIN32) || defined(_WIN64) #if defined(_WIN32) || defined(_WIN64)
#define NOMINMAX
#include <Windows.h> #include <Windows.h>
#include <memoryapi.h>
#endif #endif
namespace mimir { namespace mimir {

185
src/region.cpp Normal file
View File

@@ -0,0 +1,185 @@
//
// region.cpp
// Kraken Engine
//
// Copyright 2026 Kearwood Gilbert. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY KEARWOOD GILBERT ''AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KEARWOOD GILBERT OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The views and conclusions contained in the software and documentation are those of the
// authors and should not be interpreted as representing official policies, either expressed
// or implied, of Kearwood Gilbert.
//
#include "../include/mimir.h"
#include "mimir_impl.h"
#include <cassert>
#include <cstring>
#include <bit>
namespace mimir {
Region::Region()
: m_data(nullptr)
, m_committedSize(0)
, m_maxSize(0)
{
}
Region::~Region()
{
if (m_data) {
#if defined(_WIN32) || defined(_WIN64)
if (!VirtualFree((void*)m_data, 0, MEM_RELEASE)) {
ReportWindowsLastError("VirtualAlloc");
}
#elif defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
munmap(m_data, KRAKEN_MEM_ROUND_UP_PAGE(m_maxSize));
#else
static_assert(false, "Not Implemented");
#endif
}
}
bool Region::init(size_t maxSize)
{
assert(m_data == nullptr);
assert(m_committedSize == 0);
m_maxSize = KRAKEN_MEM_ROUND_UP_PAGE(maxSize);
#if defined(_WIN32) || defined(_WIN64)
m_data = (std::byte*)VirtualAlloc(NULL, KRAKEN_MEM_ROUND_UP_PAGE(m_maxSize), MEM_RESERVE, PAGE_NOACCESS);
if (m_data == nullptr) {
ReportWindowsLastError("VirtualAlloc");
return false;
}
#elif defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
m_data = (std::byte*)mmap(NULL, KRAKEN_MEM_ROUND_UP_PAGE(m_maxSize), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (m_data == MAP_FAILED) {
return false;
}
#else
static_assert(false, "Not Implemented");
#endif
return true;
}
bool Region::resize(size_t size)
{
size_t newSize = KRAKEN_MEM_ROUND_UP_PAGE(size);
if (newSize == m_committedSize) {
return true;
}
if (newSize > m_maxSize) {
return false;
}
if (newSize > m_committedSize) {
// ---- Growing ----
#if defined(_WIN32) || defined(_WIN64)
if (VirtualAlloc(m_data + m_committedSize, newSize - m_committedSize, MEM_COMMIT, PAGE_READWRITE) == nullptr) {
ReportWindowsLastError("VirtualAlloc");
return false;
}
#elif defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
if (mprotect(m_data + m_committedSize, newSize - m_committedSize, PROT_READ | PROT_WRITE) != 0) {
return false;
}
#else
static_assert(false, "Not Implemented");
#endif
m_committedSize = newSize;
return true;
} // if (newSize > m_committedSize)
// ---- Shrinking ----
std::byte* rangeStart = m_data + newSize;
size_t rangeLen = m_committedSize - newSize;
#if defined(_WIN32) || defined(_WIN64)
if (!VirtualAlloc(rangeStart, rangeLen, MEM_RESET, PAGE_NOACCESS)) {
ReportWindowsLastError("VirtualAlloc");
} else {
m_committedSize = newSize;
}
#elif defined(__unix__) || defined(__APPLE__) || defined(ANDROID)
if (madvise(rangeStart, rangeLen, MADV_DONTNEED) != 0) {
// TODO - Log Error or debug build assert
}
if (mprotect(rangeStart, rangeLen, PROT_NONE) != 0) {
// TODO - Log Error or debug build assert
}
#else
static_assert(false, "Not Implemented");
#endif
m_committedSize = newSize;
return true;
}
size_t Region::getSize() const
{
return m_committedSize;
}
size_t Region::getMaxSize() const
{
return m_maxSize;
}
std::byte* Region::getAddress() const
{
return m_data;
}
bool Region::maybeGrow(size_t minSize)
{
if (minSize < m_committedSize) {
// Already big enough
return true;
}
if (minSize > m_maxSize) {
// We can't grow this big
return false;
}
// We need to grow.
// Increase the size exponentially, capped to m_maxSize
size_t newSize = std::bit_ceil(minSize) << 1;
newSize = std::min(m_maxSize, newSize);
return resize(newSize);
}
void Region::maybeShrink(size_t targetSize)
{
size_t thresholdSize = targetSize << 1; // The threshold for shrinking is greater than the target to implement hysteresis
if (getSize() > thresholdSize) {
resize(targetSize);
}
}
} // namespace mimir

30
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,30 @@
# ----====---- GoogleTest ----====---
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
# ----====---- End: GoogleTest ----====---
add_executable(
mimir_test
"arena_test.cpp"
"heap_test.cpp"
"region_test.cpp"
"test_util.cpp")
target_link_libraries(
mimir_test
GTest::gtest_main
mimir
)
target_include_directories(mimir_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
include(GoogleTest)
gtest_discover_tests(mimir_test)

171
tests/arena_test.cpp Normal file
View File

@@ -0,0 +1,171 @@
#include <gtest/gtest.h>
#include <random>
#include "mimir.h"
#include "test_util.h"
namespace mimir_tests {
TEST(Arena, InitialState)
{
mimir::init();
mimir::Arena arena;
EXPECT_TRUE(arena.init());
EXPECT_EQ(arena.getUsed(), 0);
}
TEST(Arena, OutOfMemory)
{
mimir::init();
mimir::Arena arena;
EXPECT_TRUE(arena.init(1ULL << 24, 1ULL << 46));
EXPECT_TRUE(arena.alloc(1ULL << 49) == nullptr);
}
TEST(Arena, Overflow)
{
mimir::init();
mimir::Arena arena;
EXPECT_TRUE(arena.init(128, 1024));
EXPECT_TRUE(arena.alloc(512) != nullptr);
EXPECT_TRUE(arena.alloc(512) != nullptr);
EXPECT_TRUE(arena.alloc(arena.getMaxSize() - 1024 + 1) == nullptr);
}
TEST(Arena, Alignment16)
{
mimir::init();
mimir::Arena arena;
EXPECT_TRUE(arena.init());
for (int i = 0; i < 128; i++) {
for (int j = 1; j < 128; j++) {
EXPECT_TRUE(arena.alloc(i) != nullptr);
for (int k = 0; k < 16; k++) {
std::byte* addr = arena.allocA16(j);
EXPECT_TRUE(addr != nullptr);
EXPECT_EQ((size_t)addr & 0b1111, 0);
EXPECT_EQ(arena.getUsed() & 0b1111, 0);
}
arena.reset();
}
}
}
TEST(Arena, Alignment64)
{
mimir::init();
mimir::Arena arena;
EXPECT_TRUE(arena.init());
for (int i = 0; i < 128; i++) {
for (int j = 1; j < 128; j++) {
EXPECT_TRUE(arena.alloc(i) != nullptr);
for (int k = 0; k < 16; k++) {
std::byte* addr = arena.allocA64(j);
EXPECT_TRUE(addr != nullptr);
EXPECT_EQ((size_t)addr & 0b111111, 0);
EXPECT_EQ(arena.getUsed() & 0b111111, 0);
}
arena.reset();
}
}
}
TEST(Arena, OverflowByAlignmentA16)
{
mimir::init();
mimir::Arena arena;
EXPECT_TRUE(arena.init(128, 1024));
// Controlled test - first try with no alignment. This should pass.
EXPECT_TRUE(arena.alloc(alignDown(arena.getMaxSize(), 16) - 15) != nullptr);
EXPECT_TRUE(arena.alloc(arena.getMaxSize() - arena.getUsed()) != nullptr);
arena.reset();
// Now try with allocA16. This should fail.
EXPECT_TRUE(arena.alloc(alignDown(arena.getMaxSize(), 16) - 15) != nullptr);
EXPECT_TRUE(arena.allocA16(arena.getMaxSize() - arena.getUsed()) == nullptr);
}
TEST(Arena, OverflowByAlignmentA64)
{
mimir::init();
mimir::Arena arena;
EXPECT_TRUE(arena.init(128, 1024));
// Controlled test - first try with no alignment. This should pass.
EXPECT_TRUE(arena.alloc(alignDown(arena.getMaxSize(), 64) - 63) != nullptr);
EXPECT_TRUE(arena.alloc(arena.getMaxSize() - arena.getUsed()) != nullptr);
arena.reset();
// Now try with allocA64. This should fail.
EXPECT_TRUE(arena.alloc(alignDown(arena.getMaxSize(), 64) - 63) != nullptr);
EXPECT_TRUE(arena.allocA64(arena.getMaxSize() - arena.getUsed()) == nullptr);
}
TEST(Arena, FillAndReset)
{
// We purposefully want to have the same random sequence each time
const int maxAllocCount = 128;
const int cycleCount = 128;
std::mt19937_64 rgen(1234);
std::uniform_int_distribution<int> count_dist(0, maxAllocCount);
std::uniform_int_distribution<size_t> alloc_dist1(1, 256);
std::uniform_int_distribution<size_t> alloc_dist2(1, 1024 * 256);
std::uniform_int_distribution<size_t> alloc_dist3(1, 1024 * 1024 * 256);
mimir::init();
mimir::Arena arena;
EXPECT_TRUE(arena.init(0, 1ULL << 32)); // Use up to 4GB
size_t allocSize[maxAllocCount];
std::byte* allocAddr[maxAllocCount];
for (int i = 0; i < cycleCount; i++) {
int allocCount = 0;
if (i != 5) {
// Make sure at least one cycle has 0 allocations
allocCount = count_dist(rgen);
}
for (int j = 0; j < allocCount; j++) {
allocSize[j] = alloc_dist1(rgen);
}
for (int j = 0; j < 32; j++) {
int k = count_dist(rgen);
if (k < allocCount) {
allocSize[k] = alloc_dist2(rgen);
}
}
for (int j = 0; j < 3; j++) {
int k = count_dist(rgen);
if (k < allocCount) {
allocSize[k] = alloc_dist3(rgen);
}
}
size_t totalSize = 0;
for (int j = 0; j < allocCount; j++) {
totalSize += allocSize[j];
allocAddr[j] = arena.alloc(allocSize[j]);
EXPECT_TRUE(allocAddr[j] != nullptr);
if (allocAddr[j] != nullptr) {
writeData(allocAddr[j], allocSize[j], j);
}
}
for (int j = 0; j < allocCount; j++) {
if (allocAddr[j] != nullptr) {
EXPECT_TRUE(checkData(allocAddr[j], allocSize[j], j));
}
}
EXPECT_EQ(arena.getUsed(), totalSize);
arena.reset();
EXPECT_EQ(arena.getUsed(), 0);
}
}
}; // namespace mimir_tests

23
tests/heap_test.cpp Normal file
View File

@@ -0,0 +1,23 @@
#include <gtest/gtest.h>
#include "mimir.h"
#include "test_util.h"
namespace mimir_tests {
TEST(Heap, InitialState)
{
mimir::init();
mimir::Heap heap;
EXPECT_EQ(heap.getUsed(), 0);
}
TEST(Heap, OutOfMemory)
{
mimir::init();
mimir::Heap heap;
EXPECT_TRUE(heap.init(1ULL << 24, 1ULL << 46));
EXPECT_TRUE(heap.alloc(1ULL << 49) == nullptr);
}
}; // namespace mimir_tests

86
tests/region_test.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include <gtest/gtest.h>
#include "mimir.h"
#include "test_util.h"
namespace mimir_tests {
TEST(Region, InitialState)
{
mimir::init();
mimir::Region region;
EXPECT_TRUE(region.getAddress() == nullptr);
EXPECT_TRUE(region.getMaxSize() == 0);
EXPECT_TRUE(region.getSize() == 0);
}
TEST(Region, TooBig)
{
mimir::init();
mimir::Region region;
EXPECT_FALSE(region.init(1ULL << 50)); // Attempt to reserve too many pages
}
TEST(Region, OutOfMemory)
{
mimir::init();
mimir::Region region;
EXPECT_TRUE(region.init(1ULL << 46)); // Attempt to reserve 64TB of pages
EXPECT_FALSE(region.resize(1ULL << 60)); // Attempt to commit an extremely large number of pages
}
TEST(Region, Min1GB)
{
mimir::init();
mimir::Region region;
EXPECT_TRUE(region.init(1ULL << 30)); // Allocate 1GB of pages
}
TEST(Region, Resizing)
{
mimir::init();
mimir::Region region;
EXPECT_TRUE(region.init(1 << 10ULL));
EXPECT_NE(region.getAddress(), nullptr);
EXPECT_GT(region.getMaxSize(), 0);
EXPECT_EQ(region.getSize(), 0);
EXPECT_TRUE(region.resize(0));
EXPECT_EQ(region.getSize(), 0);
EXPECT_TRUE(region.resize(1ULL << 4));
EXPECT_TRUE(region.resize(1ULL << 10));
EXPECT_TRUE(region.resize(1ULL << 5));
EXPECT_TRUE(region.resize(1ULL << 0));
EXPECT_TRUE(region.resize(1ULL << 9));
}
TEST(Region, Oversized)
{
mimir::init();
mimir::Region region;
EXPECT_TRUE(region.init(1ULL << 4));
size_t prevSize = region.getSize();
EXPECT_FALSE(region.resize(1ULL << 20));
EXPECT_EQ(region.getSize(), prevSize);
}
TEST(Region, WriteData)
{
mimir::init();
mimir::Region region;
EXPECT_TRUE(region.init());
EXPECT_TRUE(region.resize(100));
EXPECT_GE(region.getSize(), 100);
writeData(region.getAddress(), 100, 1);
EXPECT_TRUE(checkData(region.getAddress(), 100, 1));
EXPECT_TRUE(region.resize(1 << 9ULL));
writeData(region.getAddress() + 100, 900, 2);
EXPECT_TRUE(checkData(region.getAddress(), 100, 1));
EXPECT_TRUE(checkData(region.getAddress() + 100, 900, 2));
}
}; // namespace mimir_tests

50
tests/test_util.cpp Normal file
View File

@@ -0,0 +1,50 @@
#include "test_util.h"
#include <algorithm>
#include <cstring>
namespace mimir_tests {
const size_t kPatternLen = 32;
void getPattern(std::byte* pattern, int seed)
{
for (int i = 0; i < kPatternLen; i++) {
pattern[i] = std::byte((seed * 3162 + i * 63163) % 0xff);
}
}
void writeData(std::byte* dest, size_t count, int seed)
{
std::byte pattern[kPatternLen];
getPattern(pattern, seed);
for (int i = 0; i < count; i += kPatternLen) {
memcpy(dest + i, pattern, std::min(count - i, kPatternLen));
}
}
bool checkData(std::byte* src, size_t count, int seed)
{
std::byte pattern[kPatternLen];
getPattern(pattern, seed);
for (int i = 0; i < count; i += kPatternLen) {
if (memcmp(src + i, pattern, std::min(count - i, kPatternLen)) != 0) {
return false;
}
}
return true;
}
size_t alignUp(size_t val, size_t alignment)
{
return alignDown(val - 1, alignment) + alignment;
}
size_t alignDown(size_t val, size_t alignment)
{
return val & ~(alignment - 1);
}
}; // namespace mimir_tests

12
tests/test_util.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include <cstddef>
namespace mimir_tests {
void writeData(std::byte* dest, size_t count, int seed);
bool checkData(std::byte* src, size_t count, int seed);
size_t alignUp(size_t val, size_t alignment);
size_t alignDown(size_t val, size_t alignment);
}; // namespace mimir_tests