Create a default MemoryAPI for MemBuffers

This commit is contained in:
2017-05-06 02:44:17 +10:00
parent 1c3c78d738
commit 6fe75928f0
3 changed files with 548 additions and 201 deletions
+139 -59
View File
@@ -69,6 +69,30 @@ DQN_FILE_SCOPE void DqnMem_Free (void *memory);
////////////////////////////////////////////////////////////////////////////////
// DqnMemBuffer - Memory Buffer, For push buffer/ptr memory style management
////////////////////////////////////////////////////////////////////////////////
// DqnMemBuffer is a data structure to dynamically allocate memory in a stack
// like style. It pre-allocates a block of memory in init and sub-allocates from
// this block to take advantage of memory locality.
// When an allocation requires a larger amount of memory than available in the
// block then the MemBuffer will allocate a new block of sufficient size for
// you in DqnMemBuffer_Allocate(..). This _DOES_ mean that there will be wasted
// space at the end of each block and is a tradeoff for memory locality against
// optimal space usage.
// How To Use:
// 1. Create a DqnMemBuffer struct and pass it into an initialisation function
// - InitWithFixedMem() allows you to pass in your own memory which is
// converted to a memory block. This disables dynamic allocation.
// NOTE: Space is reserved in the given memory for MemBufferBlock metadata.
// - InitWithFixedSize() allows you to to disable dynamic allocations and
// sub-allocate from the initial MemBuffer allocation size only.
// 2. Use DqnMemBuffer_Allocate(..) to allocate memory for use.
// - "Freeing" memory is dealt by creating temporary MemBuffers or using the
// BeginTempRegion and EndTempRegion functions. Specifically freeing
// individual items is typically not generalisable in this scheme.
typedef struct DqnMemBufferBlock
{
u8 *memory;
@@ -81,7 +105,7 @@ typedef struct DqnMemBufferBlock
enum DqnMemBufferFlag
{
DqnMemBufferFlag_IsExpandable = (1 << 0),
DqnMemBufferFlag_IsFixedMemoryFromUser = (1 << 1),
DqnMemBufferFlag_IsFixedMemoryFromUser = (1 << 1), // NOTE(doyle): Required to indicate we CAN'T free this memory when free is called.
};
typedef struct DqnMemBuffer
@@ -102,13 +126,14 @@ typedef struct DqnTempBuffer
} DqnTempBuffer;
DQN_FILE_SCOPE bool DqnMemBuffer_InitWithFixedMem (DqnMemBuffer *const buffer, u8 *const mem, const size_t memSize, const u32 byteAlign = 4); // Use preallocated memory, no further allocations, returns NULL on allocate if out of space
DQN_FILE_SCOPE bool DqnMemBuffer_InitWithFixedSize(DqnMemBuffer *const buffer, size_t size, const bool clearToZero, const u32 byteAlign = 4); // Single allocation from platform, no further allocations, returns NULL of allocate if out of space
DQN_FILE_SCOPE bool DqnMemBuffer_Init (DqnMemBuffer *const buffer, size_t size, const bool clearToZero, const u32 byteAlign = 4); // Allocates from platform dynamically as space runs out
DQN_FILE_SCOPE bool DqnMemBuffer_InitWithFixedSize(DqnMemBuffer *const buffer, size_t size, const bool zeroClear, const u32 byteAlign = 4); // Single allocation from platform, no further allocations, returns NULL of allocate if out of space
DQN_FILE_SCOPE bool DqnMemBuffer_Init (DqnMemBuffer *const buffer, size_t size, const bool zeroClear, const u32 byteAlign = 4); // Allocates from platform dynamically as space runs out
DQN_FILE_SCOPE void *DqnMemBuffer_Allocate (DqnMemBuffer *const buffer, size_t size); // Returns NULL if out of space, or platform allocation fails, or buffer is using fixed memory/size
DQN_FILE_SCOPE void DqnMemBuffer_FreeLastBuffer(DqnMemBuffer *const buffer); // Frees the last-most block to the buffer, if it's the only block, the buffer will free that block then, next allocate with attach a block.
DQN_FILE_SCOPE void DqnMemBuffer_Free (DqnMemBuffer *const buffer); // Frees all blocks belonging to this buffer
DQN_FILE_SCOPE void DqnMemBuffer_ClearCurrBlock(DqnMemBuffer *const buffer, const bool clearToZero); // Reset the current blocks usage ptr to 0
DQN_FILE_SCOPE void *DqnMemBuffer_Allocate (DqnMemBuffer *const buffer, size_t size); // Returns NULL if out of space and buffer is using fixed memory/size, or platform allocation fails
DQN_FILE_SCOPE void DqnMemBuffer_Free (DqnMemBuffer *const buffer); // Frees all blocks belonging to this buffer
DQN_FILE_SCOPE bool DqnMemBuffer_FreeBlock (DqnMemBuffer *const buffer, DqnMemBufferBlock *block); // Frees the specified block, returns false if block doesn't belong
DQN_FILE_SCOPE bool DqnMemBuffer_FreeLastBlock (DqnMemBuffer *const buffer); // Frees the last-most memory block. If last block, free that block, next allocate will attach a block.
DQN_FILE_SCOPE void DqnMemBuffer_ClearCurrBlock(DqnMemBuffer *const buffer, const bool zeroClear); // Reset the current memory block usage to 0
// TempBuffer is only required for the function. Once BeginTempRegion() is called, subsequent allocation calls can be made using the original buffer.
// Upon EndTempRegion() the original buffer will free any additional blocks it allocated during the temp region and revert to the original
@@ -122,7 +147,7 @@ DQN_FILE_SCOPE void DqnMemBuffer_EndTempRegion (DqnTempBuffer tempBuff
// DqnMemAPI - Memory API, For using custom allocators
////////////////////////////////////////////////////////////////////////////////
// You only need to care about this API if you want to use custom mem-alloc
// routines in the data structures! Otherwise it reverts to the default one.
// routines in the data structures! Otherwise it has a default one to use.
// How To Use:
// 1. Implement the callback function, where DqnMemApiCallbackInfo will tell you the request.
@@ -217,13 +242,11 @@ bool DqnArray_Init(DqnArray<T> *array, size_t capacity,
array->memAPI.callback(info, NULL);
array->data = NULL;
}
array->memAPI = memAPI;
array->memAPI = memAPI;
size_t allocateSize = (size_t)capacity * sizeof(T);
DqnMemAPICallbackResult memResult = {};
DqnMemAPICallbackInfo info =
DqnMemAPICallback_InfoAskAllocInternal(array->memAPI, allocateSize);
DqnMemAPICallbackInfo info = DqnMemAPICallback_InfoAskAllocInternal(array->memAPI, allocateSize);
array->memAPI.callback(info, &memResult);
DQN_ASSERT(memResult.type == DqnMemAPICallbackType_Alloc);
@@ -1271,16 +1294,18 @@ DQN_FILE_SCOPE void DqnMem_Free(void *memory)
////////////////////////////////////////////////////////////////////////////////
// DqnMemBuffer - Memory API, For using custom allocators
////////////////////////////////////////////////////////////////////////////////
FILE_SCOPE DqnMemBufferBlock *
DqnMemBuffer_AllocBlockInternal(u32 byteAlign, size_t size)
DQN_FILE_SCOPE DqnMemBufferBlock *
DqnMemBuffer_AllocateBlockInternal(u32 byteAlign, size_t size)
{
size_t alignedSize = DQN_ALIGN_POW_N(size, byteAlign);
size_t totalSize = alignedSize + sizeof(DqnMemBufferBlock);
size_t totalSize = alignedSize + sizeof(DqnMemBufferBlock) + (byteAlign -1);
// NOTE(doyle): Total size includes another (byteAlign-1) since we also want
// to align the base pointer to memory that we receive.
DqnMemBufferBlock *result = (DqnMemBufferBlock *)DqnMem_Calloc(totalSize);
if (!result) return NULL;
result->memory = (u8 *)result + sizeof(*result);
result->memory = (u8 *)DQN_ALIGN_POW_N((u8 *)result + sizeof(*result), byteAlign);
result->size = alignedSize;
result->used = 0;
return result;
@@ -1311,13 +1336,13 @@ DQN_FILE_SCOPE bool DqnMemBuffer_InitWithFixedMem(DqnMemBuffer *const buffer,
}
DQN_FILE_SCOPE bool DqnMemBuffer_Init(DqnMemBuffer *const buffer, size_t size,
const bool clearToZero,
const bool zeroClear,
const u32 byteAlign)
{
if (!buffer || size <= 0) return false;
DQN_ASSERT(!buffer->block);
buffer->block = DqnMemBuffer_AllocBlockInternal(byteAlign, size);
buffer->block = DqnMemBuffer_AllocateBlockInternal(byteAlign, size);
if (!buffer->block) return false;
buffer->tempBufferCount = 0;
@@ -1328,7 +1353,7 @@ DQN_FILE_SCOPE bool DqnMemBuffer_Init(DqnMemBuffer *const buffer, size_t size,
DQN_FILE_SCOPE bool DqnMemBuffer_InitWithFixedSize(DqnMemBuffer *const buffer,
size_t size,
const bool clearToZero,
const bool zeroClear,
const u32 byteAlign)
{
bool result = DqnMemBuffer_Init(buffer, size, byteAlign);
@@ -1351,11 +1376,22 @@ DQN_FILE_SCOPE void *DqnMemBuffer_Allocate(DqnMemBuffer *const buffer, size_t si
{
if (buffer->flags & DqnMemBufferFlag_IsFixedMemoryFromUser) return NULL;
// TODO: Better notifying to user, out of space in buffer
// TODO(doyle): If we make InitWithFixedSize buffer the Is_Expandable
// flag is not set. But if you free the buffery and try to allocate to
// it again since the flag is not set, the buffer becomes useless as we
// can't allocate to it anymore. How should we solve this?
if (buffer->flags & DqnMemBufferFlag_IsExpandable)
{
size_t newBlockSize = DQN_MAX(alignedSize, buffer->block->size);
DqnMemBufferBlock *newBlock = DqnMemBuffer_AllocBlockInternal(
size_t newBlockSize;
// TODO(doyle): Allocate block size based on the aligned size or
// a minimum block size? Not allocate based on the current block
// size
if (buffer->block)
newBlockSize = DQN_MAX(alignedSize, buffer->block->size);
else
newBlockSize = alignedSize;
DqnMemBufferBlock *newBlock = DqnMemBuffer_AllocateBlockInternal(
buffer->byteAlign, newBlockSize);
if (!newBlock) return NULL;
@@ -1364,6 +1400,7 @@ DQN_FILE_SCOPE void *DqnMemBuffer_Allocate(DqnMemBuffer *const buffer, size_t si
}
else
{
// TODO: Better notifying to user, out of space in buffer
return NULL;
}
}
@@ -1372,23 +1409,49 @@ DQN_FILE_SCOPE void *DqnMemBuffer_Allocate(DqnMemBuffer *const buffer, size_t si
u8 *alignedResult = (u8 *)DQN_ALIGN_POW_N(currPointer, buffer->byteAlign);
size_t alignmentOffset = (size_t)(alignedResult - currPointer);
// NOTE(doyle): Since all buffers can't change alignment once they've been
// initialised and that the base memory ptr is already aligned, then all
// subsequent allocations should also be aligned automatically.
// TODO(doyle): In the future, do we want to allow arbitrary alignment PER
// allocation, not per MemBuffer?
DQN_ASSERT(alignmentOffset == 0);
void *result = alignedResult;
buffer->block->used += (alignedSize + alignmentOffset);
return result;
}
DQN_FILE_SCOPE void
DqnMemBuffer_FreeLastBuffer(DqnMemBuffer *const buffer)
DQN_FILE_SCOPE bool DqnMemBuffer_FreeBlock(DqnMemBuffer *const buffer,
DqnMemBufferBlock *block)
{
if (buffer->flags & DqnMemBufferFlag_IsFixedMemoryFromUser) return;
if (!buffer || !block || !buffer->block) return false;
if (buffer->flags & DqnMemBufferFlag_IsFixedMemoryFromUser) return false;
DqnMemBufferBlock *prevBlock = buffer->block->prevBlock;
DqnMem_Free(buffer->block);
buffer->block = prevBlock;
DqnMemBufferBlock **blockPtr = &buffer->block;
// No more blocks, then last block has been freed
if (!buffer->block) DQN_ASSERT(buffer->tempBufferCount == 0);
while (*blockPtr && (*blockPtr) != block)
blockPtr = &((*blockPtr)->prevBlock);
if (*blockPtr)
{
DqnMemBufferBlock *blockToFree = *blockPtr;
(*blockPtr) = blockToFree->prevBlock;
DqnMem_Free(blockToFree);
// No more blocks, then last block has been freed
if (!buffer->block) DQN_ASSERT(buffer->tempBufferCount == 0);
return true;
}
return false;
}
DQN_FILE_SCOPE bool
DqnMemBuffer_FreeLastBlock(DqnMemBuffer *const buffer)
{
bool result = DqnMemBuffer_FreeBlock(buffer, buffer->block);
return result;
}
DQN_FILE_SCOPE void DqnMemBuffer_Free(DqnMemBuffer *buffer)
@@ -1397,19 +1460,17 @@ DQN_FILE_SCOPE void DqnMemBuffer_Free(DqnMemBuffer *buffer)
if (buffer->flags & DqnMemBufferFlag_IsFixedMemoryFromUser) return;
while (buffer->block)
{
DqnMemBuffer_FreeLastBuffer(buffer);
}
DqnMemBuffer_FreeLastBlock(buffer);
}
DQN_FILE_SCOPE void DqnMemBuffer_ClearCurrBlock(DqnMemBuffer *const buffer,
const bool clearToZero)
const bool zeroClear)
{
if (!buffer) return;
if (buffer->block)
{
buffer->block->used = 0;
if (clearToZero)
if (zeroClear)
{
DqnMem_Clear(buffer->block->memory, 0,
buffer->block->size);
@@ -1433,7 +1494,7 @@ DQN_FILE_SCOPE void DqnMemBuffer_EndTempRegion(DqnTempBuffer tempBuffer)
{
DqnMemBuffer *buffer = tempBuffer.buffer;
while (buffer->block != tempBuffer.startingBlock)
DqnMemBuffer_FreeLastBuffer(buffer);
DqnMemBuffer_FreeLastBlock(buffer);
if (buffer->block)
{
@@ -1483,7 +1544,6 @@ FILE_SCOPE DqnMemAPICallbackInfo DqnMemAPICallback_InfoAskFreeInternal(
void DqnMemAPI_ValidateCallbackInfo(DqnMemAPICallbackInfo info)
{
DQN_ASSERT(!info.userContext);
DQN_ASSERT(info.type != DqnMemAPICallbackType_Invalid);
switch(info.type)
@@ -1513,6 +1573,7 @@ FILE_SCOPE void
DqnMemAPI_DefaultUseCallocCallbackInternal(DqnMemAPICallbackInfo info,
DqnMemAPICallbackResult *result)
{
DQN_ASSERT(!info.userContext);
DqnMemAPI_ValidateCallbackInfo(info);
switch(info.type)
{
@@ -1547,6 +1608,7 @@ FILE_SCOPE void
DqnMemAPI_DefaultUseMemBufferCallbackInternal(DqnMemAPICallbackInfo info,
DqnMemAPICallbackResult *result)
{
DQN_ASSERT(info.userContext);
DqnMemAPI_ValidateCallbackInfo(info);
DqnMemBuffer *const buffer = static_cast<DqnMemBuffer *>(info.userContext);
@@ -1560,38 +1622,55 @@ DqnMemAPI_DefaultUseMemBufferCallbackInternal(DqnMemAPICallbackInfo info,
}
break;
// TODO(doyle): This is so badly handled. Currently MemBuffers are not
// designed to handle realloc in anyway elegantly due to it's memory
// block nature.
case DqnMemAPICallbackType_Realloc:
{
// NOTE(doyle): Realloc implies that our data must be contiguous.
// We also assert that the supplied memory buffer is brand new, i.e.
// the memory buffer is used exclusively for the MemAPI operations.
// So if we don't have enough space, we can free the entire memory
// buffer and reallocate.
// NOTE(doyle): Realloc implies that we want our data to be
// contiguous. We also enforce (assert) in init that the supplied
// memory buffer is brand new, i.e. the memory buffer is used
// exclusively for the MemAPI operations. So if we don't have enough
// space, we can free the entire memory buffer and reallocate
// without having to worry about part of blocks holding data
// belonging to other objects.
DQN_ASSERT(result);
DQN_ASSERT(!buffer->block->prevBlock);
DQN_ASSERT(info.oldMemPtr == buffer->block->memory);
result->type = info.type;
// TODO(doyle): In regards to above, we have no way of ensuring that
// the user doesn't use this buffer elsewhere, which would
// invalidate all our assumptions. We can fix this maybe, by making
// the DefaultUseMemBuffer allocate its own DqnMemBuffer privately
// that the user can't see externally to enforce this invariant.
DqnMemBufferBlock tmpCopy = *buffer->block;
result->type = info.type;
size_t alignedSize = DQN_ALIGN_POW_N(info.newRequestSize, buffer->byteAlign);
if ((buffer->block->used + alignedSize) > buffer->block->size)
DqnMemBuffer_Free(buffer);
result->newMemPtr = DqnMemBuffer_Allocate(buffer, alignedSize);
if (!result->newMemPtr)
if (alignedSize > buffer->block->size)
{
// TODO(doyle): This should be big warning. If it failed, system
// must be out of memory. Since we are using a push buffer, and
// we don't clear to zero on free AND we ensure that there's
// only ever a singular block used.. then technically we can
// restore data by restoring the block metadata which should
// preserve the old data living here.
*buffer->block = tmpCopy;
DqnMemBufferBlock *oldBlock = buffer->block;
result->newMemPtr = DqnMemBuffer_Allocate(buffer, alignedSize);
if (result->newMemPtr)
{
DqnMemBufferBlock *newBlock = buffer->block;
DQN_ASSERT(oldBlock != newBlock);
for (u32 i = 0; i < oldBlock->used; i++)
newBlock->memory[i] = oldBlock->memory[i];
newBlock->used = oldBlock->used;
DqnMemBuffer_FreeBlock(buffer, oldBlock);
}
}
else
{
// Otherwise, the current block still has enough space so we can
// realloc in place.
// TODO(doyle): Somewhat hacky, we clear the curr block pointer,
// and don't zero it out, and just reallocate the aligned size.
DqnMemBuffer_ClearCurrBlock(buffer, false);
result->newMemPtr = DqnMemBuffer_Allocate(buffer, alignedSize);
}
DQN_ASSERT(!buffer->block->prevBlock);
}
@@ -1602,6 +1681,7 @@ DqnMemAPI_DefaultUseMemBufferCallbackInternal(DqnMemAPICallbackInfo info,
// TODO(doyle): Freeing technically works, if we enforce that the
// MemBuffer exclusively belongs to the MemAPI it's linked to.
if (result) result->type = info.type;
DQN_ASSERT(info.ptrToFree == buffer->block->memory);
DqnMemBuffer_Free(buffer);
}
break;
@@ -1620,8 +1700,8 @@ DQN_FILE_SCOPE DqnMemAPI DqnMemAPI_DefaultUseMemBuffer(DqnMemBuffer *const buffe
{
// TODO(doyle): We assert that the buffer has to be a brand new mem buffer.
// Is this the correct design choice?
DQN_ASSERT(buffer && buffer->block);
DQN_ASSERT(!buffer->block->prevBlock && buffer->block->used == 0);
DQN_ASSERT(buffer);
DqnMemAPI result = {};
result.callback = DqnMemAPI_DefaultUseMemBufferCallbackInternal;