Compare commits

..

10 Commits

Author SHA1 Message Date
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
13 changed files with 726 additions and 117 deletions

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,39 @@
#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();
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

73
include/heap.h Normal file
View File

@@ -0,0 +1,73 @@
//
// 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 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);
private:
Region m_region;
// Add a free block to the index
void insertFreeBlock(TLSFBlock* block);
// Remove a free block from the index
void removeFreeBlock(TLSFBlock* block);
};
} // 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 {

58
include/region.h Normal file
View File

@@ -0,0 +1,58 @@
//
// 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;
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,64 +49,31 @@ 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
}
} }
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 (neededSize > m_region.getSize())
if (m_data == 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)
{ {
// Increase the size exponentially when we run out
size_t newSize = std::bit_ceil(neededSize) << 1; size_t newSize = std::bit_ceil(neededSize) << 1;
if (newSize < m_minSize) { if (newSize < m_minSize) {
newSize = m_minSize; newSize = m_minSize;
} }
newSize = KRAKEN_MEM_ROUND_UP_PAGE(newSize); m_region.resize(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; std::byte* ret = m_region.getAddress() + m_usedSize;
m_usedSize = neededSize; m_usedSize = neededSize;
return ret; return ret;
} }
@@ -142,22 +82,22 @@ std::byte* Arena::allocA16(size_t size)
{ {
uint32_t nextByte = (m_usedSize + 15) & ~15; uint32_t nextByte = (m_usedSize + 15) & ~15;
uint32_t roundedSize = (size + 15) & ~15; uint32_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; uint32_t nextByte = (m_usedSize + 63) & ~63;
uint32_t roundedSize = (size + 63) & ~63; uint32_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()
@@ -176,28 +116,8 @@ void Arena::reset()
targetSize = m_minSize; targetSize = m_minSize;
} }
size_t thresholdSize = targetSize << 1; // The threshold for shrinking is greater than the target to implement hysteresis size_t thresholdSize = targetSize << 1; // The threshold for shrinking is greater than the target to implement hysteresis
if (m_committedSize > thresholdSize) { if (m_region.getSize() > thresholdSize) {
size_t newSize = KRAKEN_MEM_ROUND_UP_PAGE(targetSize); m_region.resize(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

304
src/heap.cpp Normal file
View File

@@ -0,0 +1,304 @@
//
// 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 <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()
{
}
Heap::~Heap()
{
}
bool Heap::init(size_t maxSize)
{
if (!m_region.init(maxSize)) {
return false;
}
if (!m_region.resize(sizeof(TLSFIndex) + 16)) {
return false;
}
TLSFIndex* index = (TLSFIndex*)m_region.getAddress();
memset(index, 0, sizeof(TLSFIndex));
// Start with one free block, filling the entire Region
TLSFBlock* block = (TLSFBlock*)(m_region.getAddress() + sizeof(TLSFIndex));
block->nextFree = nullptr;
block->prevFree = nullptr;
block->size = m_region.getMaxSize() - sizeof(TLSFIndex);
block->size |= 0b11; // T=1: Last Block, F=1: Free Block
insertFreeBlock(block);
return true;
}
void Heap::insertFreeBlock(TLSFBlock* block)
{
// Add the block to the index
TLSFIndex* index = (TLSFIndex*)m_region.getAddress();
size_t usableSize = (block->size & ~0b11) - 16;
size_t firstLevelIndex = std::bit_width(usableSize) - 5;
size_t secondLevelIndex = (usableSize >> firstLevelIndex) & 0b1111ULL;
index->firstLevelFreeBitMap |= std::bit_floor(usableSize);
index->secondLevelFreeBitMap[firstLevelIndex] |= secondLevelIndex & 0b1111ULL;
TLSFBlock* prevFirstFreeBlock = index->secondLevelFreeBlocks[firstLevelIndex][secondLevelIndex];
index->secondLevelFreeBlocks[firstLevelIndex][secondLevelIndex] = block;
block->prevFree = nullptr;
if (prevFirstFreeBlock) {
block->nextFree = prevFirstFreeBlock;
prevFirstFreeBlock->prevFree = block;
}
}
void Heap::removeFreeBlock(TLSFBlock* block)
{
// Remove the block from the index
TLSFIndex* index = (TLSFIndex*)m_region.getAddress();
size_t usableSize = (block->size & ~0b11) - 16;
size_t firstLevel = std::bit_width(usableSize);
size_t firstLevelIndex = firstLevel - 5;
size_t secondLevelIndex = (usableSize >> (firstLevel - 5)) & 0b1111ULL;
if (block->prevFree != nullptr) {
// Link the neighboring free blocks together
block->prevFree = block->nextFree;
if (block->nextFree) {
block->nextFree = block->prevFree;
}
} else {
// This block was the first for this level
index->secondLevelFreeBlocks[firstLevelIndex][secondLevelIndex] = block->nextFree;
if (block->nextFree == nullptr) {
// We have removed all the 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);
}
}
}
if (block->size & 0b10ULL) {
// This block was the last in physical order.
if (block->prevPhys) {
// Update the prior block in physical order to mark it as the last block.
block->prevPhys->size &= 0b10ULL;
}
}
}
// Allocate `size` bytes
std::byte* Heap::alloc(size_t size)
{
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.
selectedSecondLevel = std::countr_zero(index->secondLevelFreeBitMap[selectedFirstLevel - 4]);
}
// Take the first free block
TLSFBlock* block = index->secondLevelFreeBlocks[selectedFirstLevel - 4][selectedSecondLevel];
size_t oldBlockSize = block->size & ~0b11ULL;
bool oldBlockWasLastPhysBlock = (block->size & ~0b10ULL) != 0;
TLSFBlock* prevFreeBlock = block->prevFree;
TLSFBlock* nextFreeBlock = block->nextFree;
TLSFBlock* prevPhysBlock = block->prevPhys;
TLSFBlock* nextPhysBlock = nullptr;
if (!oldBlockWasLastPhysBlock) {
nextPhysBlock = (TLSFBlock*)(((std::byte*)block) + oldBlockSize + 16);
}
// The next free block at this level replaces this block in the index.
index->secondLevelFreeBlocks[selectedFirstLevel - 4][selectedSecondLevel] = nextFreeBlock;
if (nextFreeBlock == nullptr) {
// This was the last free block at this level.
// Update the bitmap for the second level...
index->secondLevelFreeBitMap[selectedFirstLevel - 4] &= ~(1ULL << selectedSecondLevel);
// Check if there are any remaining free blocks within the first level
if (index->secondLevelFreeBitMap[selectedFirstLevel - 4] == 0) {
// This was the last one. Clear the first level bit as well.
index->firstLevelFreeBitMap &= ~(1ULL << selectedFirstLevel);
}
} else {
// The next free block is now the first free block at this level.
nextFreeBlock->prevFree = nullptr;
}
if (nextPhysBlock) {
// Link the next physical block to this one.
nextPhysBlock->prevPhys = block;
} else {
// This is the last physical block, so set the last block bit.
block->size &= 0b10ULL;
}
block->size = size;
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)
{
}
} // 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,16 @@
#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)
#include <Windows.h> #include <Windows.h>
#include <memoryapi.h>
#endif #endif
namespace mimir { namespace mimir {

153
src/region.cpp Normal file
View File

@@ -0,0 +1,153 @@
//
// 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_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;
}
};

28
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,28 @@
# ----====---- 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)
# ----====---- End: GoogleTest ----====---
enable_testing()
add_executable(
mimir_test
"region_test.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)

55
tests/region_test.cpp Normal file
View File

@@ -0,0 +1,55 @@
#include <gtest/gtest.h>
#include "mimir.h"
void writeData(std::byte* dest, size_t count, int seed)
{
for (int i = 0; i < count; i++) {
dest[i] = std::byte((seed * 3162 + i * 63163) % 0xff);
}
}
bool checkData(std::byte* src, size_t count, int seed)
{
for (int i = 0; i < count; i++) {
if (src[i] != std::byte((seed * 3162 + i * 63163) % 0xff)) {
return false;
}
}
return true;
}
TEST(Mimir_Region, Uninitialized) {
mimir::init();
mimir::Region region;
EXPECT_TRUE(region.getAddress() == nullptr);
EXPECT_TRUE(region.getMaxSize() == 0);
EXPECT_TRUE(region.getSize() == 0);
}
TEST(Mimir_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_FALSE(region.resize(1 << 20ULL));
EXPECT_EQ(region.getSize(), 0);
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));
EXPECT_TRUE(region.resize(1 << 10ULL));
EXPECT_TRUE(region.resize(1 << 5ULL));
EXPECT_TRUE(region.resize(1 << 0ULL));
EXPECT_TRUE(region.resize(1 << 9ULL));
}