Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

game-buf: rewrite to use arena allocator #2496

Merged
merged 1 commit into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/tr1/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.1...develop) - ××××-××-××
- improved memory usage by shedding ca. 100-110 MB on average

## [4.8.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.8...tr1-4.8.1) - 2025-02-14
- fixed loading non-Caves saves triggering a new save prompt when save crystals are enabled (#2498, regression from 4.8)
Expand Down
1 change: 1 addition & 0 deletions docs/tr2/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-0.9...develop) - ××××-××-××
- improved memory usage by shedding ca. 100-110 MB on average

## [0.9](https://github.com/LostArtefacts/TRX/compare/tr2-0.8...tr2-0.9) - 2025-02-14
- added Linux builds and toolchain (#1598)
Expand Down
38 changes: 7 additions & 31 deletions src/libtrx/game/game_buf.c
Original file line number Diff line number Diff line change
@@ -1,52 +1,28 @@
#include "game/game_buf.h"

#include "enum_map.h"
#include "game/shell.h"
#include "log.h"
#include "memory.h"

static size_t m_Cap = 0;
static size_t m_MemUsed = 0;
static size_t m_MemFree = 0;
static char *m_MemBase = nullptr;
static char *m_MemPtr = nullptr;
static MEMORY_ARENA_ALLOCATOR m_Allocator = {
.default_chunk_size = 1024 * 1024 * 5,
rr- marked this conversation as resolved.
Show resolved Hide resolved
};

void GameBuf_Init(const size_t cap)
void GameBuf_Init(void)
{
m_Cap = cap;
m_MemBase = Memory_Alloc(cap);
GameBuf_Reset();
}

void GameBuf_Reset(void)
{
m_MemPtr = m_MemBase;
m_MemFree = m_Cap;
m_MemUsed = 0;
Memory_ArenaReset(&m_Allocator);
}

void GameBuf_Shutdown(void)
{
Memory_FreePointer(&m_MemBase);
m_Cap = 0;
m_MemFree = 0;
m_MemUsed = 0;
Memory_ArenaFree(&m_Allocator);
}

void *GameBuf_Alloc(const size_t alloc_size, const GAME_BUFFER buffer)
{
const size_t aligned_size = (alloc_size + 3) & ~3;

const char *const buffer_name = ENUM_MAP_TO_STRING(GAME_BUFFER, buffer);
rr- marked this conversation as resolved.
Show resolved Hide resolved
if (aligned_size > m_MemFree) {
Shell_ExitSystemFmt(
"Ran out of memory while trying to allocate %d bytes for %s",
aligned_size, buffer_name);
}

void *const result = m_MemPtr;
m_MemFree -= aligned_size;
m_MemUsed += aligned_size;
m_MemPtr += aligned_size;
return result;
return Memory_ArenaAlloc(&m_Allocator, aligned_size);
}
15 changes: 8 additions & 7 deletions src/libtrx/include/libtrx/game/game_buf.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

#include <stddef.h>

// Internal game memory manager. It allocates its internal buffer once per
// level launch. All subsequent "allocation" requests operate with pointer
// arithmetic. This makes it fast and convenient to request more memory as we
// go, but it makes freeing memory really inconvenient which is why it is
// intentionally not implemented. To use more dynamic memory management, use
// Memory_Alloc / Memory_Free.
// Internal game memory manager using an arena allocator. Memory is allocated
// in discrete chunks, with each allocation request served via pointer
// arithmetic within the active chunk. When a request exceeds the current
// chunk's capacity, a new chunk is allocated to continue servicing
// allocations. This design offers very fast allocation speeds, but individual
// blocks cannot be freed – only the entire arena can be reset when needed. For
// more granular memory management, use Memory_Alloc / Memory_Free.

typedef enum {
// clang-format off
Expand Down Expand Up @@ -51,7 +52,7 @@ typedef enum {
// clang-format on
} GAME_BUFFER;

void GameBuf_Init(size_t cap);
void GameBuf_Init(void);
void GameBuf_Shutdown(void);
void GameBuf_Reset(void);

Expand Down
29 changes: 29 additions & 0 deletions src/libtrx/include/libtrx/memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@
// Basic memory utilities that exit the game in case the system runs out of
// memory.

// Arena allocator - a buffer that only grows, until it's reset. Doesn't
// support freeing while in-use.
typedef struct MEMORY_ARENA_CHUNK {
void *memory;
size_t size;
size_t offset;
struct MEMORY_ARENA_CHUNK *next;
} MEMORY_ARENA_CHUNK;

typedef struct {
MEMORY_ARENA_CHUNK *first_chunk;
MEMORY_ARENA_CHUNK *current_chunk;
size_t default_chunk_size;
} MEMORY_ARENA_ALLOCATOR;

// Allocate n bytes. In case the memory allocation fails, shows an error to the
// user and exits the application. The allocated memory is filled with zeros.
void *Memory_Alloc(size_t size);
Expand Down Expand Up @@ -40,3 +55,17 @@ char *Memory_Dup(const char *buffer, size_t size);
// the user and exits the application. The string must be nullptr-terminated.
// Giving a nullptr to this function is a fatal error.
char *Memory_DupStr(const char *string);

// Allocate n bytes using the arena allocator. If there's insufficient memory,
// grow the buffer using internal growth function. The allocated memory is
// filled with zeros.
void *Memory_ArenaAlloc(MEMORY_ARENA_ALLOCATOR *allocator, size_t size);

// Resets the buffer used by the arena allocator, but does not free the memory.
// allocator must not be a nullptr. Used to reset the buffer, but not suffer
// from performance penalty associated with reallocating the actual memory.
void Memory_ArenaReset(MEMORY_ARENA_ALLOCATOR *allocator);

// Frees the entire buffer owned by the arena allocator. allocator must not be
// nullptr.
void Memory_ArenaFree(MEMORY_ARENA_ALLOCATOR *allocator);
72 changes: 72 additions & 0 deletions src/libtrx/memory.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
#include "memory.h"

#include "debug.h"
#include "utils.h"

#include <stdlib.h>
#include <string.h>

static MEMORY_ARENA_CHUNK *M_ArenaAllocChunk(
MEMORY_ARENA_ALLOCATOR *allocator, size_t size);

static MEMORY_ARENA_CHUNK *M_ArenaAllocChunk(
MEMORY_ARENA_ALLOCATOR *const allocator, const size_t size)
{
const size_t new_chunk_size = MAX(allocator->default_chunk_size, size);
MEMORY_ARENA_CHUNK *const new_chunk =
Memory_Alloc(sizeof(MEMORY_ARENA_CHUNK) + new_chunk_size);
new_chunk->memory = (char *)new_chunk + sizeof(MEMORY_ARENA_CHUNK);
new_chunk->size = new_chunk_size;
new_chunk->offset = 0;
new_chunk->next = nullptr;
return new_chunk;
}

void *Memory_Alloc(const size_t size)
{
void *result = malloc(size);
Expand Down Expand Up @@ -51,3 +68,58 @@ char *Memory_DupStr(const char *const string)
strcpy(memory, string);
return memory;
}

void *Memory_ArenaAlloc(
MEMORY_ARENA_ALLOCATOR *const allocator, const size_t size)
{
// Ensure a default chunk size is set.
if (allocator->default_chunk_size == 0) {
allocator->default_chunk_size = 1024 * 4; // default to 4K
}

// Find first chunk that has enough space.
MEMORY_ARENA_CHUNK *chunk = allocator->current_chunk;
while (chunk != nullptr && chunk->offset + size > chunk->size) {
chunk = chunk->next;
}

// If no chunk satisfies this criteria, append a new chunk.
if (chunk == nullptr) {
chunk = M_ArenaAllocChunk(allocator, size);
if (allocator->current_chunk != nullptr) {
chunk->next = allocator->current_chunk->next;
allocator->current_chunk->next = chunk;
}
allocator->current_chunk = chunk;
if (allocator->first_chunk == nullptr) {
allocator->first_chunk = chunk;
}
}

ASSERT(chunk != nullptr);

// Allocate from the current chunk.
void *const result = (char *)chunk->memory + chunk->offset;
chunk->offset += size;
return result;
}

void Memory_ArenaReset(MEMORY_ARENA_ALLOCATOR *const allocator)
{
MEMORY_ARENA_CHUNK *chunk = allocator->first_chunk;
while (chunk != nullptr) {
chunk->offset = 0;
chunk = chunk->next;
}
allocator->current_chunk = allocator->first_chunk;
rr- marked this conversation as resolved.
Show resolved Hide resolved
}

void Memory_ArenaFree(MEMORY_ARENA_ALLOCATOR *const allocator)
{
MEMORY_ARENA_CHUNK *chunk = allocator->first_chunk;
while (chunk != nullptr) {
MEMORY_ARENA_CHUNK *const next = chunk->next;
Memory_Free(chunk);
chunk = next;
}
}
3 changes: 1 addition & 2 deletions src/tr1/game/shell.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
#include <stdio.h>
#include <string.h>

#define GAMEBUF_MEM_CAP 0x8000000
#define TIMESTAMP_SIZE 20

typedef enum {
Expand Down Expand Up @@ -133,7 +132,7 @@ void Shell_Init(
Savegame_Init();
Savegame_ScanSavedGames();
Savegame_HighlightNewestSlot();
GameBuf_Init(GAMEBUF_MEM_CAP);
GameBuf_Init();
Console_Init();
}

Expand Down
4 changes: 1 addition & 3 deletions src/tr2/game/shell/common.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@
#include <stdarg.h>
#include <stdio.h>

#define GAMEBUF_MEM_CAP 0x8000000

static Uint64 m_UpdateDebounce = 0;
static const char *m_CurrentGameFlowPath = "cfg/TR2X_gameflow.json5";
static const char *m_CurrentGameStringsPath = "cfg/TR2X_strings.json5";
Expand Down Expand Up @@ -359,7 +357,7 @@ void Shell_Main(void)
Savegame_InitCurrentInfo();
S_FrontEndCheck();

GameBuf_Init(GAMEBUF_MEM_CAP);
GameBuf_Init();

GF_COMMAND gf_cmd = GF_DoFrontendSequence();
bool loop_continue = !Shell_IsExiting();
Expand Down